From 1d3256186dfb5b408dc534e49a136ae73d3607a8 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Mon, 28 Oct 2024 19:54:18 +0100 Subject: [PATCH 01/15] Provide the code with more detailed documentation comments --- .../Framework/Environment/Environment.swift | 24 ++++++++++++----- .../Environment/EnvironmentKeys.swift | 7 +++++ .../Environment/EnvironmentModifier.swift | 18 +++++++------ .../Environment/EnvironmentObject.swift | 26 +++++++++++++------ .../Environment/EnvironmentValue.swift | 11 ++++++-- .../Primitives/Elements/Element.swift | 22 ++++++++++++---- 6 files changed, 78 insertions(+), 30 deletions(-) diff --git a/Sources/HTMLKit/Framework/Environment/Environment.swift b/Sources/HTMLKit/Framework/Environment/Environment.swift index 32f89fac..eb0413a8 100644 --- a/Sources/HTMLKit/Framework/Environment/Environment.swift +++ b/Sources/HTMLKit/Framework/Environment/Environment.swift @@ -1,30 +1,36 @@ import Foundation -/// A type that represents the environment +/// A class that represents the environment +/// +/// The environment provides storage for various settings used by the renderer public class Environment { /// The storage of the environment private var storage: [AnyKeyPath: Any] - /// Initiates a manager + /// Initializes the environment public init() { self.storage = [:] } - /// The current time zone of the environment + /// The current time zone of the environment public var timeZone: TimeZone? - /// The current calender of the environment + /// The current calendar of the environment public var calendar: Calendar? - /// The current local of the environment + /// The current locale of the environment public var locale: Locale? /// The current color scheme of the environment public var colorScheme: String? - /// Retrieves an item from storage by its path + /// Retrieves a value from environment for a given key path + /// + /// - Parameter path: The key path used to look up the value + /// + /// - Returns: The value public func retrieve(for path: AnyKeyPath) -> Any? { if let value = self.storage[path] { @@ -34,7 +40,11 @@ public class Environment { return nil } - /// Adds und updates an item to the storage + /// Inserts or updates a value in the environment for the given key path + /// + /// - Parameters: + /// - value: The value to be stored or updated + /// - path: The key path that identifies where the value is stored public func upsert(_ value: T, for path: AnyKeyPath) { self.storage[path] = value } diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentKeys.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentKeys.swift index df68b701..39e5f4b9 100644 --- a/Sources/HTMLKit/Framework/Environment/EnvironmentKeys.swift +++ b/Sources/HTMLKit/Framework/Environment/EnvironmentKeys.swift @@ -1,12 +1,19 @@ import Foundation +/// A set of predefined environment keys +/// +/// The keys are used on the environment modifiers to configure various settings for the environment. public struct EnvironmentKeys: Hashable { + /// A key used to configure the environment's calendar public var calender: Calendar + /// A key used to configure the environment's time zone public var timeZone: TimeZone + /// A key used to configure the environment's locale public var locale: Locale + /// A key used to configure the environment's color scheme public var colorScheme: String } diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift index 06a8aab6..3d9a15d3 100644 --- a/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift +++ b/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift @@ -1,9 +1,6 @@ -/* - Abstract: - The file contains the environment modifier. - */ - -/// A type that contains the value and the following content, after modifing the environment. +/// A type that holds the modification details for the environment +/// +/// The modifier is received by the renderer and applied to the environment. @_documentation(visibility: internal) public struct EnvironmentModifier: Content { @@ -13,10 +10,15 @@ public struct EnvironmentModifier: Content { /// The environment value public var value: Any? - /// The following content + /// The sub-content public var content: [Content] - /// Initiates a environment modifier + /// Initializes an environment modifier + /// + /// - Parameters: + /// - key: The key path of the environment value to be modified + /// - value: The new value to update the environment value with + /// - content: The sub-content to be rendered public init(key: AnyKeyPath, value: Any? = nil, content: [Content]) { self.key = key diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentObject.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentObject.swift index d030f9ce..f47aa845 100644 --- a/Sources/HTMLKit/Framework/Environment/EnvironmentObject.swift +++ b/Sources/HTMLKit/Framework/Environment/EnvironmentObject.swift @@ -1,18 +1,20 @@ import Foundation -/// A property wrapper type to initate an environment object +/// A property wrapper type to provide access to an environment object +/// +/// An environment object allows to read shared environment data. @frozen @propertyWrapper public struct EnvironmentObject { - /// The wrapped value + /// The wrapped object public var wrappedValue: Wrapper - /// Converts the type into the wrapped value + /// Initialiizes the environment object public init(_ type: ObjectType.Type) { self.wrappedValue = .init() } - /// A type, that holds the environment object informationen + /// A wrapper, that holds the object informationen @dynamicMemberLookup public struct Wrapper { /// The path of the parent @@ -21,20 +23,26 @@ import Foundation /// The path of the value internal var path: AnyKeyPath - /// Initiates a wrapper + /// Initializes the wrapper public init() { self.path = \WrapperType.self } - /// Initiates a wrapper with the necessary information for the environment object + /// Initializes the wrapper + /// + /// - Parameters: + /// - parent: The path of the parent + /// - path: The path of the value internal init(parent: AnyKeyPath, path: AnyKeyPath) { self.parent = parent self.path = path } - /// Looks up for a containing property + /// Accesses a wrapped value for a given key path dynamically + /// + /// - Returns: An environment value public subscript(dynamicMember member: KeyPath) -> EnvironmentValue { guard let newPath = self.path.appending(path: member) else { @@ -48,7 +56,9 @@ import Foundation return .init(parentPath: self.path, valuePath: newPath) } - /// Looks up for a containing model + /// Accesses a wrapped model object for a given key path dynamically + /// + /// - Returns: An environment value public subscript(dynamicMember member: KeyPath) -> Wrapper where T: ViewModel { guard let newPath = self.path.appending(path: member) else { diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift index e9022c67..033ace20 100644 --- a/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift +++ b/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift @@ -1,6 +1,8 @@ import Foundation -/// A type, that acts as a binding value +/// A type that serves as a placeholder for an environment value +/// +/// The placeholder will be evaluated and resolved by the renderer when needed. public struct EnvironmentValue: Content { /// The path of the values parent @@ -9,7 +11,11 @@ public struct EnvironmentValue: Content { /// The path of the value internal var valuePath: AnyKeyPath - /// Initiates a environment value + /// Initializes a environment value + /// + /// - Parameters: + /// - parentPath: The key path of the parent + /// - valuePath: The key path of the value public init(parentPath: AnyKeyPath, valuePath: AnyKeyPath) { self.parentPath = parentPath @@ -19,6 +25,7 @@ public struct EnvironmentValue: Content { extension EnvironmentValue { + /// Concatenates an environment value with another value static public func + (lhs: Content, rhs: Self) -> Content { return [lhs, rhs] } diff --git a/Sources/HTMLKit/Framework/Primitives/Elements/Element.swift b/Sources/HTMLKit/Framework/Primitives/Elements/Element.swift index 106e569c..e678ff83 100644 --- a/Sources/HTMLKit/Framework/Primitives/Elements/Element.swift +++ b/Sources/HTMLKit/Framework/Primitives/Elements/Element.swift @@ -1,8 +1,3 @@ -/* - Abstract: - The file contains the default definition of an element. It defines which properties and methods an element should come with. - */ - /// A type that represents any html-element. @_documentation(visibility: internal) public protocol Element: Content { @@ -10,14 +5,31 @@ public protocol Element: Content { extension Element { + /// Sets the environment for the sub-content + /// + /// - Parameter key: The key + /// + /// - Returns: The environment modifier public func environment(key: KeyPath) -> EnvironmentModifier { return .init(key: key, content: [self]) } + /// Supplies a value to the environment + /// + /// - Parameters: + /// - key: The key to store the value with + /// - value: The value to be stored + /// + /// - Returns: The environment modifier public func environment(key: KeyPath, value: V) -> EnvironmentModifier { return .init(key: key, value: value, content: [self]) } + /// Supplies the object to the environment + /// + /// - Parameter object: The object to be stored + /// + /// - Returns: The environment modifier public func environment(object: T) -> EnvironmentModifier { return .init(key: \T.self, value: object, content: [self]) } From 62e02e3624cf94d273f215f7ecee77600963bbb3 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Mon, 28 Oct 2024 19:54:58 +0100 Subject: [PATCH 02/15] Fix typo --- Sources/HTMLKit/Framework/Environment/EnvironmentKeys.swift | 2 +- Sources/HTMLKit/Framework/Rendering/Renderer.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentKeys.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentKeys.swift index 39e5f4b9..39c2154a 100644 --- a/Sources/HTMLKit/Framework/Environment/EnvironmentKeys.swift +++ b/Sources/HTMLKit/Framework/Environment/EnvironmentKeys.swift @@ -6,7 +6,7 @@ import Foundation public struct EnvironmentKeys: Hashable { /// A key used to configure the environment's calendar - public var calender: Calendar + public var calendar: Calendar /// A key used to configure the environment's time zone public var timeZone: TimeZone diff --git a/Sources/HTMLKit/Framework/Rendering/Renderer.swift b/Sources/HTMLKit/Framework/Rendering/Renderer.swift index 2be7f67e..f44d860d 100644 --- a/Sources/HTMLKit/Framework/Rendering/Renderer.swift +++ b/Sources/HTMLKit/Framework/Rendering/Renderer.swift @@ -380,7 +380,7 @@ public final class Renderer { case \.locale: self.environment.locale = value as? Locale - case \.calender: + case \.calendar: self.environment.calendar = value as? Calendar case \.timeZone: From 4bc848a93d854cdcfac868938effd3d73513f518 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Mon, 28 Oct 2024 21:25:18 +0100 Subject: [PATCH 03/15] Refactor the code a bit --- .../Framework/Environment/Environment.swift | 39 ++++++--- .../Environment/EnvironmentModifier.swift | 6 +- .../Framework/Rendering/Renderer.swift | 25 ------ Tests/HTMLKitTests/EnvironmentTests.swift | 85 ++++++++++--------- Tests/HTMLKitVaporTests/ProviderTests.swift | 6 +- 5 files changed, 81 insertions(+), 80 deletions(-) diff --git a/Sources/HTMLKit/Framework/Environment/Environment.swift b/Sources/HTMLKit/Framework/Environment/Environment.swift index eb0413a8..fa73971f 100644 --- a/Sources/HTMLKit/Framework/Environment/Environment.swift +++ b/Sources/HTMLKit/Framework/Environment/Environment.swift @@ -3,7 +3,7 @@ import Foundation /// A class that represents the environment /// /// The environment provides storage for various settings used by the renderer -public class Environment { +public final class Environment { /// The storage of the environment private var storage: [AnyKeyPath: Any] @@ -15,16 +15,36 @@ public class Environment { } /// The current time zone of the environment - public var timeZone: TimeZone? + public var timeZone: TimeZone? { + + get { + retrieve(for: \EnvironmentKeys.timeZone) as? TimeZone + } + } /// The current calendar of the environment - public var calendar: Calendar? + public var calendar: Calendar? { + + get { + retrieve(for: \EnvironmentKeys.calendar) as? Calendar + } + } /// The current locale of the environment - public var locale: Locale? + public var locale: Locale? { + + get { + retrieve(for: \EnvironmentKeys.locale) as? Locale + } + } /// The current color scheme of the environment - public var colorScheme: String? + public var colorScheme: String? { + + get { + retrieve(for: \EnvironmentKeys.colorScheme) as? String + } + } /// Retrieves a value from environment for a given key path /// @@ -32,12 +52,7 @@ public class Environment { /// /// - Returns: The value public func retrieve(for path: AnyKeyPath) -> Any? { - - if let value = self.storage[path] { - return value - } - - return nil + return storage[path] } /// Inserts or updates a value in the environment for the given key path @@ -46,6 +61,6 @@ public class Environment { /// - value: The value to be stored or updated /// - path: The key path that identifies where the value is stored public func upsert(_ value: T, for path: AnyKeyPath) { - self.storage[path] = value + storage[path] = value } } diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift index 3d9a15d3..cca4b822 100644 --- a/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift +++ b/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift @@ -5,13 +5,13 @@ public struct EnvironmentModifier: Content { /// The environment key - public var key: AnyKeyPath + internal var key: AnyKeyPath /// The environment value - public var value: Any? + internal var value: Any? /// The sub-content - public var content: [Content] + internal var content: [Content] /// Initializes an environment modifier /// diff --git a/Sources/HTMLKit/Framework/Rendering/Renderer.swift b/Sources/HTMLKit/Framework/Rendering/Renderer.swift index f44d860d..e7fda879 100644 --- a/Sources/HTMLKit/Framework/Rendering/Renderer.swift +++ b/Sources/HTMLKit/Framework/Rendering/Renderer.swift @@ -369,31 +369,6 @@ public final class Renderer { if let value = modifier.value { self.environment.upsert(value, for: modifier.key) - - } else { - - if let value = self.environment.retrieve(for: modifier.key) { - - if let key = modifier.key as? PartialKeyPath { - - switch key { - case \.locale: - self.environment.locale = value as? Locale - - case \.calendar: - self.environment.calendar = value as? Calendar - - case \.timeZone: - self.environment.timeZone = value as? TimeZone - - case \.colorScheme: - self.environment.colorScheme = value as? String - - default: - throw Errors.unindendedEnvironmentKey - } - } - } } return try render(contents: modifier.content) diff --git a/Tests/HTMLKitTests/EnvironmentTests.swift b/Tests/HTMLKitTests/EnvironmentTests.swift index 85176dd9..795dd5e5 100644 --- a/Tests/HTMLKitTests/EnvironmentTests.swift +++ b/Tests/HTMLKitTests/EnvironmentTests.swift @@ -1,64 +1,71 @@ -/* - Abstract: - The file tests the annotations. - */ - import HTMLKit import XCTest final class EnvironmentTests: XCTestCase { - struct Object: Encodable { - - var title: String = "Welcome to WWDC" - var name: String = "Mattes!" - var image: String = "wwdc.jpeg" - } + var renderer = Renderer() - struct ParentView: View { + /// Tests the environment access through the environment object + /// + /// The renderer is expected to evaluate the placeholder and renderer the resulting value. + func testEnvironmentAccess() throws { - var content: [Content] + struct FamilyObject: ViewModel { + + let name: String = "Doe" + let father: FatherObject = FatherObject() + } - init(@ContentBuilder content: () -> [Content]) { - self.content = content() + struct FatherObject: ViewModel { + + let avatar: String = "john_doe.jpeg" + let name: String = "John" } - var body: Content { - Division { - content + struct ParentView: View { + + let content: [Content] + + init(@ContentBuilder content: () -> [Content]) { + self.content = content() + } + + var body: Content { + Division { + content + } + .environment(object: FamilyObject()) } - .environment(object: Object()) } - } - - struct ChildView: View { - @EnvironmentObject(Object.self) - var object - - var body: Content { - ParentView { - Section{ - Image() - .source(object.image) - Heading2 { - object.title + " " + object.name + struct ChildView: View { + + @EnvironmentObject(FamilyObject.self) + var object + + var body: Content { + ParentView { + Section{ + Image() + .source(object.father.avatar) + Heading1 { + object.name + } + Paragraph { + object.father.name + " " + object.name + } } } } } - } - - var renderer = Renderer() - - func testEnvironment() throws { XCTAssertEqual(try renderer.render(view: ChildView()), """
\
\ - \ -

Welcome to WWDC Mattes!

\ + \ +

Doe

\ +

John Doe

\
\
""" diff --git a/Tests/HTMLKitVaporTests/ProviderTests.swift b/Tests/HTMLKitVaporTests/ProviderTests.swift index 135ec129..0df9c9e1 100644 --- a/Tests/HTMLKitVaporTests/ProviderTests.swift +++ b/Tests/HTMLKitVaporTests/ProviderTests.swift @@ -42,7 +42,6 @@ final class ProviderTests: XCTestCase { content } } - .environment(object: TestObject()) } } @@ -192,12 +191,17 @@ final class ProviderTests: XCTestCase { } } + /// Tests the access to environment through provider + /// + /// The provider is expected to recieve the environment object and resolve it based on the request. func testEnvironmentIntegration() throws { let app = Application(.testing) defer { app.shutdown() } + app.htmlkit.environment.upsert(TestObject(), for: \TestObject.self) + app.get("test") { request async throws -> Vapor.View in return try await request.htmlkit.render(TestPage.SipplingView()) } From dabe61393f597e282b99f7383ed22714c6990bd9 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Fri, 3 Jan 2025 11:33:30 +0100 Subject: [PATCH 04/15] Use the right environment key --- Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift b/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift index fd8f5bfa..b768eb40 100644 --- a/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift +++ b/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift @@ -112,7 +112,7 @@ extension Request { public var htmlkit: ViewRenderer { if let acceptLanguage = self.acceptLanguage { - self.application.htmlkit.environment.locale = HTMLKit.Locale(tag: acceptLanguage) + self.application.htmlkit.environment.upsert(HTMLKit.Locale(tag: acceptLanguage), for: \HTMLKit.EnvironmentKeys.locale) } return .init(eventLoop: self.eventLoop, configuration: self.application.htmlkit.configuration, logger: self.logger) From 587bac5bfa8f0a35e920a1ccbb057816421a48cc Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Fri, 3 Jan 2025 12:09:02 +0100 Subject: [PATCH 05/15] Add a conditional statement to handle environment specific conditions --- .../Framework/Environment/Condition.swift | 42 ++++ .../Framework/Environment/Conditionable.swift | 4 + .../Framework/Environment/Environment.swift | 26 ++ .../Environment/EnvironmentValue.swift | 32 ++- .../Framework/Environment/Relation.swift | 60 +++++ .../Framework/Environment/Statement.swift | 26 ++ .../Extensions/Comparable+HTMLKit.swift | 52 ++++ .../Framework/Rendering/Renderer.swift | 125 ++++++++++ Tests/HTMLKitTests/EnvironmentTests.swift | 229 ++++++++++++++++++ 9 files changed, 594 insertions(+), 2 deletions(-) create mode 100644 Sources/HTMLKit/Framework/Environment/Condition.swift create mode 100644 Sources/HTMLKit/Framework/Environment/Conditionable.swift create mode 100644 Sources/HTMLKit/Framework/Environment/Relation.swift create mode 100644 Sources/HTMLKit/Framework/Environment/Statement.swift create mode 100644 Sources/HTMLKit/Framework/Extensions/Comparable+HTMLKit.swift diff --git a/Sources/HTMLKit/Framework/Environment/Condition.swift b/Sources/HTMLKit/Framework/Environment/Condition.swift new file mode 100644 index 00000000..1bbece69 --- /dev/null +++ b/Sources/HTMLKit/Framework/Environment/Condition.swift @@ -0,0 +1,42 @@ +/// A type representing a condition that compares an environment value against another value +@_documentation(visibility: internal) +public struct Condition: Conditionable { + + /// A enumeration of potential comparison + public enum Comparison { + + /// Indicates an equal comparison + case equal + + /// Indicates a not-equal comparison + case unequal + + /// Indicates a greater-than comparison + case greater + + /// Indicates a less-than comparison + case less + } + + /// The left-hand side value + public let lhs: EnvironmentValue + + /// The right-hand side value to test against + public let rhs: any Comparable + + /// The comparison to perfom + public let comparison: Comparison + + /// Initializes a condition + /// + /// - Parameters: + /// - lhs: The origin value + /// - rhs: The value to atest against + /// - operation: The comparison to perfom + public init(lhs: EnvironmentValue, rhs: any Comparable, comparison: Comparison) { + + self.lhs = lhs + self.rhs = rhs + self.comparison = comparison + } +} diff --git a/Sources/HTMLKit/Framework/Environment/Conditionable.swift b/Sources/HTMLKit/Framework/Environment/Conditionable.swift new file mode 100644 index 00000000..772eb7d3 --- /dev/null +++ b/Sources/HTMLKit/Framework/Environment/Conditionable.swift @@ -0,0 +1,4 @@ +/// A type that defines a conditonal value which will be evualuated by the renderer. +@_documentation(visibility: internal) +public protocol Conditionable { +} diff --git a/Sources/HTMLKit/Framework/Environment/Environment.swift b/Sources/HTMLKit/Framework/Environment/Environment.swift index fa73971f..dd6be60b 100644 --- a/Sources/HTMLKit/Framework/Environment/Environment.swift +++ b/Sources/HTMLKit/Framework/Environment/Environment.swift @@ -64,3 +64,29 @@ public final class Environment { storage[path] = value } } + +extension Environment { + + /// Evaluates one condition + /// + /// - Parameters: + /// - condition: The condition to evaluate + /// - content: The content for the true statement + /// + /// - Returns: A environment condition + public static func when(_ condition: Conditionable, @ContentBuilder content: () -> [Content]) -> Statement { + return Statement(compound: condition, first: content(), second: []) + } + + /// Evaluates one condition + /// + /// - Parameters: + /// - condition: The condition to evaluate + /// - content: The content for the true statement + /// - then: The content for the false statement + /// + /// - Returns: A environment condition + public static func when(_ condition: Conditionable, @ContentBuilder content: () -> [Content], @ContentBuilder then: () -> [Content]) -> Statement { + return Statement(compound: condition, first: content(), second: then()) + } +} diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift index 033ace20..1a62dbd9 100644 --- a/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift +++ b/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift @@ -25,8 +25,36 @@ public struct EnvironmentValue: Content { extension EnvironmentValue { - /// Concatenates an environment value with another value - static public func + (lhs: Content, rhs: Self) -> Content { + /// Concat environment value with environment value + public static func + (lhs: Content, rhs: Self) -> Content { return [lhs, rhs] } + + /// Compare an environment value with another comparable value + /// + /// Makes an unequal evaluation + public static func != (lhs: Self, rhs: some Comparable) -> Condition { + return Condition(lhs: lhs, rhs: rhs, comparison: .unequal) + } + + /// Compare an environment value with another comparable value + /// + /// Makes an equal evaluation + public static func == (lhs: Self, rhs: some Comparable) -> Condition { + return Condition(lhs: lhs, rhs: rhs, comparison: .equal) + } + + /// Compare an environment value with another comparable value + /// + /// Makes an less than evaluation + public static func < (lhs: Self, rhs: some Comparable) -> Condition { + return Condition(lhs: lhs, rhs: rhs, comparison: .less) + } + + /// Compare an environment value with another comparable value + /// + /// Makes an greater than evaluation + public static func > (lhs: Self, rhs: some Comparable) -> Condition { + return Condition(lhs: lhs, rhs: rhs, comparison: .greater) + } } diff --git a/Sources/HTMLKit/Framework/Environment/Relation.swift b/Sources/HTMLKit/Framework/Environment/Relation.swift new file mode 100644 index 00000000..10a1d0b6 --- /dev/null +++ b/Sources/HTMLKit/Framework/Environment/Relation.swift @@ -0,0 +1,60 @@ +/// A type representing the logical relation between two conditionals +@_documentation(visibility: internal) +public struct Relation: Conditionable { + + /// A enumeration of potential logical terms + public enum Term { + + /// Indicates a conjunction + /// + /// All conditions must be true for the relation to be true + case conjunction + + /// Indicates a disjunction + /// + /// One condition must be at least true for the relation to be true + case disjunction + } + + /// The logical term specifying the relation + public let term: Term + + /// The left-hand side conditional + public let lhs: Conditionable + + /// The right-hand side conditional + public let rhs: Conditionable +} + +/// Creates a conjunctional relation between two conditionals +/// +/// ```swift +/// Environment.when(value > 0 && value < 2) { +/// } +/// ``` +/// +/// - Parameters: +/// - lhs: The left-hand side conditional +/// - rhs: The right-hand side conditional +/// +/// - Returns: A conjunctional relation +public func && (lhs: Conditionable, rhs: Conditionable) -> Relation { + return Relation(term: .conjunction, lhs: lhs, rhs: rhs) +} + +/// Creates a disjunctional relation between two conditionals +/// +/// ```swift +/// Environment.when(value > 0 || value < 2) { +/// } +/// ``` +/// +/// - Parameters: +/// - lhs: The left-hand side conditional +/// - rhs: The right-hand side conditional +/// +/// - Returns: A disjunctional relation +public func || (lhs: Conditionable, rhs: Conditionable) -> Relation { + return Relation(term: .disjunction, lhs: lhs, rhs: rhs) +} + diff --git a/Sources/HTMLKit/Framework/Environment/Statement.swift b/Sources/HTMLKit/Framework/Environment/Statement.swift new file mode 100644 index 00000000..e58d5b1a --- /dev/null +++ b/Sources/HTMLKit/Framework/Environment/Statement.swift @@ -0,0 +1,26 @@ +/// A type representing a conditional block within the environment +@_documentation(visibility: internal) +public struct Statement: Content { + + /// The compound condition + let compound: Conditionable + + /// The first statement + let first: [Content] + + /// The second statement + let second: [Content] + + /// Initializes a statement + /// + /// - Parameters: + /// - compound: The compound of conditionals + /// - first: The statement to execute if conditionals are true + /// - second: The statement to execute if conditionals are false + init(compound: Conditionable, first: [Content], second: [Content]) { + + self.compound = compound + self.first = first + self.second = second + } +} diff --git a/Sources/HTMLKit/Framework/Extensions/Comparable+HTMLKit.swift b/Sources/HTMLKit/Framework/Extensions/Comparable+HTMLKit.swift new file mode 100644 index 00000000..d3b6dde6 --- /dev/null +++ b/Sources/HTMLKit/Framework/Extensions/Comparable+HTMLKit.swift @@ -0,0 +1,52 @@ +extension Comparable { + + /// Checks for equality + /// + /// - Parameters: + /// - other: The other value to compare + /// + /// - Returns: The result + public func equal(_ other: Any) -> Bool { + + guard let other = other as? Self else { + return false + } + + return other == self + } + + /// Checks for inequality + /// + /// - Parameters: + /// - other: The other value to compare + /// + /// - Returns: The result + public func unequal(_ other: Any) -> Bool { + return !equal(other) + } + + /// Checks for a greater value + /// + /// - Parameters: + /// - other: The other value to compare + /// + /// - Returns: The result + public func greater(_ other: Any) -> Bool { + + guard let other = other as? Self else { + return false + } + + return other > self + } + + /// Checks for smaller value + /// + /// - Parameters: + /// - other: The other value to compare + /// + /// - Returns: The result + public func less(_ other: Any) -> Bool { + return !greater(other) + } +} diff --git a/Sources/HTMLKit/Framework/Rendering/Renderer.swift b/Sources/HTMLKit/Framework/Rendering/Renderer.swift index 2eba02bd..e610e9d0 100644 --- a/Sources/HTMLKit/Framework/Rendering/Renderer.swift +++ b/Sources/HTMLKit/Framework/Rendering/Renderer.swift @@ -136,6 +136,13 @@ public final class Renderer { result += escape(content: try render(value: value)) } + if let statement = content as? Statement { + + if let yield = try render(statement: statement) { + result += yield + } + } + if let string = content as? MarkdownString { if !features.contains(.markdown) { @@ -211,6 +218,13 @@ public final class Renderer { result += escape(content: try render(value: value)) } + if let statement = content as? Statement { + + if let yield = try render(statement: statement) { + result += yield + } + } + if let string = content as? MarkdownString { if !features.contains(.markdown) { @@ -329,6 +343,13 @@ public final class Renderer { result += escape(content: try render(value: value)) } + if let statement = content as? Statement { + + if let yield = try render(statement: statement) { + result += yield + } + } + if let string = content as? MarkdownString { if !features.contains(.markdown) { @@ -428,6 +449,110 @@ public final class Renderer { throw Errors.unableToCastEnvironmentValue } } + + /// Renders a environment statement + private func render(statement: Statement) throws -> String? { + + var result = false + + if let condition = statement.compound as? Condition { + result = try render(condition: condition) + } + + if let relation = statement.compound as? Relation { + result = try render(relation: relation) + } + + if result { + return try render(contents: statement.first) + } + + return try render(contents: statement.second) + } + + private func render(relation: Relation) throws -> Bool { + + switch relation.term { + case .conjunction: + + var result = true + + if let condition = relation.lhs as? Condition { + result = try render(condition: condition) + } + + if let relation = relation.lhs as? Relation { + result = try render(relation: relation) + } + + if !result { + /// Bail early if the first result already is false + return result + } + + if let condition = relation.rhs as? Condition { + result = try render(condition: condition) + } + + if let relation = relation.rhs as? Relation { + result = try render(relation: relation) + } + + return result + + case .disjunction: + + var result = false + + if let condition = relation.lhs as? Condition { + result = try render(condition: condition) + } + + if let relation = relation.lhs as? Relation { + result = try render(relation: relation) + } + + if result { + /// Bail early if the first result is already true + return result + } + + if let condition = relation.rhs as? Condition { + result = try render(condition: condition) + } + + if let relation = relation.rhs as? Relation { + result = try render(relation: relation) + } + + return result + } + } + + private func render(condition: Condition) throws -> Bool { + + guard let parent = self.environment.retrieve(for: condition.lhs.parentPath) else { + throw Errors.environmentObjectNotFound + } + + guard let value = parent[keyPath: condition.lhs.valuePath] else { + throw Errors.environmentValueNotFound + } + + switch condition.comparison { + case .equal: + return condition.rhs.equal(value) + + case .greater: + return condition.rhs.greater(value) + + case .unequal: + return condition.rhs.unequal(value) + + case .less: + return condition.rhs.less(value) + } + } /// Renders the node attributes. private func render(attributes: OrderedDictionary) throws -> String { diff --git a/Tests/HTMLKitTests/EnvironmentTests.swift b/Tests/HTMLKitTests/EnvironmentTests.swift index 795dd5e5..7d5e7c13 100644 --- a/Tests/HTMLKitTests/EnvironmentTests.swift +++ b/Tests/HTMLKitTests/EnvironmentTests.swift @@ -71,4 +71,233 @@ final class EnvironmentTests: XCTestCase { """ ) } + + /// Tests condtion evaluation for the environment + /// + /// The renderer is expected to evaluated the condition correctly and renderer the right statement based on the condition. + func testEnvironmentCondition() throws { + + struct TestObject: ViewModel { + + let firstName: String = "Jane" + let lastName: String = "Doe" + let age: Int = 40 + } + + struct TestView: View { + + @EnvironmentObject(TestObject.self) + var object + + var body: Content { + Paragraph { + + // Should return false + Environment.when(object.firstName == "John") { + "True" + } then: { + "False" + } + + // The counter test, should return true + Environment.when(object.firstName == "Jane") { + "True" + } + + // Should return true + Environment.when(object.firstName != "John") { + "True" + } + + // The counter test, should return false + Environment.when(object.firstName != "Jane") { + "True" + } then: { + "False" + } + + // Should return false + Environment.when(object.age > 41) { + "True" + } then: { + "False" + } + + // The counter test, should return true + Environment.when(object.age > 39) { + "True" + } + + // Should return true + Environment.when(object.age < 41) { + "True" + } + + // The counter test, should return false + Environment.when(object.age < 39) { + "True" + } then: { + "False" + } + } + .environment(object: TestObject()) + } + } + + XCTAssertEqual(try renderer.render(view: TestView()), + """ +

FalseTrueTrueFalseFalseTrueTrueFalse

+ """ + ) + } + + /// Tests the evaluation of a statement with a conjunctional relation + func testConditionConjuction() throws { + + struct TestObject: ViewModel { + + let firstName: String = "Jane" + let lastName: String = "Doe" + let age: Int = 40 + } + + struct TestView: View { + + @EnvironmentObject(TestObject.self) + var object + + var body: Content { + Paragraph { + + // The relation is true, cause both conditions are true + Environment.when(object.age > 39 && object.age < 41) { + "True" + } then: { + "False" + } + + // The relation is false, cause both conditions are false + Environment.when(object.age < 39 && object.age > 41) { + "True" + } then: { + "False" + } + + // The relation is false, cause the first condition is false + Environment.when(object.age > 41 && object.age > 39) { + "True" + } then: { + "False" + } + + // The relation is false, cause the second condition is false + Environment.when(object.age > 39 && object.age > 41) { + "True" + } then: { + "False" + } + } + .environment(object: TestObject()) + } + } + + XCTAssertEqual(try renderer.render(view: TestView()), + """ +

TrueFalseFalseFalse

+ """ + ) + } + + /// Tests the evaluation of a statement with a disjunctional relation + func testConditionDisjunction() throws { + + struct TestObject: ViewModel { + + let firstName: String = "Jane" + let lastName: String = "Doe" + let age: Int = 40 + } + + struct TestView: View { + + @EnvironmentObject(TestObject.self) + var object + + var body: Content { + Paragraph { + + // The relation is true, cause the second condition is true + Environment.when(object.age > 41 || object.age == 40) { + "True" + } then: { + "False" + } + + // The relation is true, cause the first condition is true + Environment.when(object.age == 40 || object.age > 41) { + "True" + } then: { + "False" + } + + // The relation is false, cause all conditions are false + Environment.when(object.age == 50 || object.age > 41) { + "True" + } then: { + "False" + } + } + .environment(object: TestObject()) + } + } + + XCTAssertEqual(try renderer.render(view: TestView()), + """ +

TrueTrueFalse

+ """ + ) + } + + /// Tests the evaluation of various relation combinations + func testConditionConjunctionAndDisjunction() throws { + + struct TestObject: ViewModel { + + let firstName = "Jane" + let lastName = "Doe" + let age = 40 + } + + struct TestView: View { + + @EnvironmentObject(TestObject.self) + var object + + var body: Content { + Paragraph { + + // The statement is true, cause the first relation is true + Environment.when(object.age == 40 || object.age > 41 && object.age < 39) { + "True" + } then: { + "False" + } + + // The statement is true, cause the second relation is true + Environment.when(object.age == 50 || object.age < 41 && object.age > 39) { + "True" + } then: { + "False" + } + } + .environment(object: TestObject()) + } + } + + XCTAssertEqual(try renderer.render(view: TestView()), + """ +

TrueTrue

+ """ + ) + } } From fe9b6c377aedd21df93bacacb57720dfd8239790 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Fri, 3 Jan 2025 13:04:00 +0100 Subject: [PATCH 06/15] Add a loop statement to handle environment specific sequences --- .../Framework/Environment/Environment.swift | 11 ++ .../Framework/Environment/Sequence.swift | 20 +++ .../Framework/Rendering/Renderer.swift | 125 ++++++++++++++++++ Tests/HTMLKitTests/EnvironmentTests.swift | 36 +++++ 4 files changed, 192 insertions(+) create mode 100644 Sources/HTMLKit/Framework/Environment/Sequence.swift diff --git a/Sources/HTMLKit/Framework/Environment/Environment.swift b/Sources/HTMLKit/Framework/Environment/Environment.swift index dd6be60b..b8720657 100644 --- a/Sources/HTMLKit/Framework/Environment/Environment.swift +++ b/Sources/HTMLKit/Framework/Environment/Environment.swift @@ -89,4 +89,15 @@ extension Environment { public static func when(_ condition: Conditionable, @ContentBuilder content: () -> [Content], @ContentBuilder then: () -> [Content]) -> Statement { return Statement(compound: condition, first: content(), second: then()) } + + /// Iterates through a sequence of values + /// + /// - Parameters: + /// - sequence: The sequence to iterate over + /// - content: The content for the iteration + /// + /// - Returns: A environment condition + public static func loop(_ sequence: EnvironmentValue, @ContentBuilder content: (EnvironmentValue) -> [Content]) -> Sequence { + return Sequence(value: sequence, content: content(sequence)) + } } diff --git a/Sources/HTMLKit/Framework/Environment/Sequence.swift b/Sources/HTMLKit/Framework/Environment/Sequence.swift new file mode 100644 index 00000000..800986e9 --- /dev/null +++ b/Sources/HTMLKit/Framework/Environment/Sequence.swift @@ -0,0 +1,20 @@ +/// A type representing a loop rendering content within the environment +public struct Sequence: Content { + + /// The environment value of the sequence + let value: EnvironmentValue + + /// The accumulated content + let content: [Content] + + /// Initializes a loop + /// + /// - Parameters: + /// - value: The environment value to retrieve the sequence from + /// - content: The content to render for each item in the sequence + init(value: EnvironmentValue, content: [Content]) { + + self.value = value + self.content = content + } +} diff --git a/Sources/HTMLKit/Framework/Rendering/Renderer.swift b/Sources/HTMLKit/Framework/Rendering/Renderer.swift index e610e9d0..4582fcf6 100644 --- a/Sources/HTMLKit/Framework/Rendering/Renderer.swift +++ b/Sources/HTMLKit/Framework/Rendering/Renderer.swift @@ -143,6 +143,10 @@ public final class Renderer { } } + if let loop = content as? Sequence { + result += try render(loop: loop) + } + if let string = content as? MarkdownString { if !features.contains(.markdown) { @@ -218,6 +222,10 @@ public final class Renderer { result += escape(content: try render(value: value)) } + if let loop = content as? Sequence { + result += try render(loop: loop) + } + if let statement = content as? Statement { if let yield = try render(statement: statement) { @@ -350,6 +358,10 @@ public final class Renderer { } } + if let loop = content as? Sequence { + result += try render(loop: loop) + } + if let string = content as? MarkdownString { if !features.contains(.markdown) { @@ -603,4 +615,117 @@ public final class Renderer { return value } + + private func render(loop: Sequence) throws -> String { + + guard let parent = self.environment.retrieve(for: loop.value.parentPath) else { + throw Errors.environmentObjectNotFound + } + + guard let values = parent[keyPath: loop.value.valuePath] as? (any Swift.Sequence) else { + throw Errors.environmentValueNotFound + } + + var result = "" + + for value in values { + try render(loop: loop.content, with: value, on: &result) + } + + return result + } + + private func render(loop contents: [Content], with value: Any, on result: inout String) throws { + + for content in contents { + + if let element = content as? (any ContentNode) { + try render(loop: element, with: value, on: &result) + } + + if let element = content as? (any CustomNode) { + try render(loop: element, with: value, on: &result) + } + + if let element = content as? (any EmptyNode) { + result += try render(element: element) + } + + if let element = content as? (any CommentNode) { + result += render(element: element) + } + + if let string = content as? LocalizedString { + result += try render(localized: string) + } + + if let string = content as? MarkdownString { + + if !features.contains(.markdown) { + result += escape(content: string.raw) + + } else { + result += try render(markdown: string) + } + } + + if let element = content as? String { + result += escape(content: element) + } + + if content is EnvironmentValue { + + switch value { + case let floatValue as Float: + result += String(floatValue) + + case let intValue as Int: + result += String(intValue) + + case let doubleValue as Double: + result += String(doubleValue) + + case let stringValue as String: + result += stringValue + + default: + break + } + } + } + } + + private func render(loop element: some ContentNode, with value: Any, on result: inout String) throws { + + result += "<\(element.name)" + + if let attributes = element.attributes { + result += try render(attributes: attributes) + } + + result += ">" + + if let contents = element.content as? [Content] { + try render(loop: contents, with: value, on: &result) + } + + result += "" + } + + private func render(loop element: some CustomNode, with value: Any, on result: inout String) throws { + + result += "<\(element.name)" + + if let attributes = element.attributes { + result += try render(attributes: attributes) + } + + result += ">" + + if let contents = element.content as? [Content] { + try render(loop: contents, with: value, on: &result) + } + + result += "" + } } diff --git a/Tests/HTMLKitTests/EnvironmentTests.swift b/Tests/HTMLKitTests/EnvironmentTests.swift index 7d5e7c13..2cb61169 100644 --- a/Tests/HTMLKitTests/EnvironmentTests.swift +++ b/Tests/HTMLKitTests/EnvironmentTests.swift @@ -300,4 +300,40 @@ final class EnvironmentTests: XCTestCase { """ ) } + + /// Tests the iteration over a sequence of environment values + func testEnvironmentLoop() throws { + + struct TestObject: ViewModel { + + let name: String = "Jane" + let children: [String] = ["Janek", "Janet"] + } + + struct TestView: View { + + @EnvironmentObject(TestObject.self) + var object + + var body: Content { + Paragraph { + Environment.loop(object.children) { child in + Paragraph { + child + } + } + } + .environment(object: TestObject()) + } + } + + XCTAssertEqual(try renderer.render(view: TestView()), + """ +

\ +

Janek

\ +

Janet

\ +

+ """ + ) + } } From 60483d4228267422da3ba51fb5c15f258a69035f Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Fri, 3 Jan 2025 13:58:02 +0100 Subject: [PATCH 07/15] Make string interpolation of a environment value possible --- .../Environment/EnvironmentString.swift | 31 +++++++++++++++++++ .../Framework/Rendering/Renderer.swift | 25 +++++++++++++++ Tests/HTMLKitTests/EnvironmentTests.swift | 30 ++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 Sources/HTMLKit/Framework/Environment/EnvironmentString.swift diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentString.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentString.swift new file mode 100644 index 00000000..c80b5840 --- /dev/null +++ b/Sources/HTMLKit/Framework/Environment/EnvironmentString.swift @@ -0,0 +1,31 @@ +/// A type that represents an environment string +@_documentation(visibility: internal) +public struct EnvironmentString: ExpressibleByStringLiteral, ExpressibleByStringInterpolation, Content { + + internal var values: [Content] = [] + + public init(stringLiteral: String) { + self.values.append(stringLiteral) + } + + public init(stringInterpolation: StringInterpolation) { + self.values.append(contentsOf: stringInterpolation.values) + } + + public struct StringInterpolation: StringInterpolationProtocol { + + internal var values: [Content] = [] + + public init(literalCapacity: Int, interpolationCount: Int) { + values.reserveCapacity(interpolationCount) + } + + public mutating func appendLiteral(_ literal: String) { + values.append(literal) + } + + public mutating func appendInterpolation(_ env: EnvironmentValue) { + values.append(env) + } + } +} diff --git a/Sources/HTMLKit/Framework/Rendering/Renderer.swift b/Sources/HTMLKit/Framework/Rendering/Renderer.swift index 4582fcf6..1e410392 100644 --- a/Sources/HTMLKit/Framework/Rendering/Renderer.swift +++ b/Sources/HTMLKit/Framework/Rendering/Renderer.swift @@ -157,6 +157,10 @@ public final class Renderer { } } + if let envstring = content as? EnvironmentString { + result += try render(envstring: envstring) + } + if let element = content as? String { result += escape(content: element) } @@ -243,6 +247,10 @@ public final class Renderer { } } + if let envstring = content as? EnvironmentString { + result += try render(envstring: envstring) + } + if let element = content as? String { result += escape(content: element) } @@ -372,6 +380,10 @@ public final class Renderer { } } + if let envstring = content as? EnvironmentString { + result += try render(envstring: envstring) + } + if let element = content as? String { result += escape(content: element) } @@ -593,6 +605,11 @@ public final class Renderer { return self.markdown.render(string: escape(content: markdown.raw)) } + /// Renders a environment interpolation + private func render(envstring: EnvironmentString) throws -> String { + return try render(contents: envstring.values) + } + /// Converts specific charaters into encoded values. private func escape(attribute value: String) -> String { @@ -669,6 +686,10 @@ public final class Renderer { } } + if let envstring = content as? EnvironmentString { + result += try render(envstring: envstring) + } + if let element = content as? String { result += escape(content: element) } @@ -728,4 +749,8 @@ public final class Renderer { result += "" } + + private func render(loop envstring: EnvironmentString, with value: Any, on result: inout String) throws { + try render(loop: envstring.values, with: value, on: &result) + } } diff --git a/Tests/HTMLKitTests/EnvironmentTests.swift b/Tests/HTMLKitTests/EnvironmentTests.swift index 2cb61169..b5161346 100644 --- a/Tests/HTMLKitTests/EnvironmentTests.swift +++ b/Tests/HTMLKitTests/EnvironmentTests.swift @@ -336,4 +336,34 @@ final class EnvironmentTests: XCTestCase { """ ) } + + /// Tests the string interpolation with an environment value + /// + /// The renderer is expected to render the string correctly + func testStringInterpolationWithEnvironment() throws { + + struct TestObject: ViewModel { + + let name: String = "Jane" + } + + struct TestView: View { + + @EnvironmentObject(TestObject.self) + var object + + var body: Content { + Paragraph { + EnvironmentString("Hello, how are you \(object.name)?") + } + .environment(object: TestObject()) + } + } + + XCTAssertEqual(try renderer.render(view: TestView()), + """ +

Hello, how are you Jane?

+ """ + ) + } } From f6ecd7ac190f43c619a492dbd44d44e531cd25bb Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Mon, 6 Jan 2025 09:55:21 +0100 Subject: [PATCH 08/15] Make environmentvalue conditionable --- .../Environment/EnvironmentValue.swift | 2 +- .../Framework/Environment/Negation.swift | 23 +++++++++ .../Framework/Rendering/Renderer.swift | 47 +++++++++++++++++++ Tests/HTMLKitTests/EnvironmentTests.swift | 15 +++++- 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 Sources/HTMLKit/Framework/Environment/Negation.swift diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift index 1a62dbd9..5e36e159 100644 --- a/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift +++ b/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift @@ -23,7 +23,7 @@ public struct EnvironmentValue: Content { } } -extension EnvironmentValue { +extension EnvironmentValue: Conditionable { /// Concat environment value with environment value public static func + (lhs: Content, rhs: Self) -> Content { diff --git a/Sources/HTMLKit/Framework/Environment/Negation.swift b/Sources/HTMLKit/Framework/Environment/Negation.swift new file mode 100644 index 00000000..129d7f42 --- /dev/null +++ b/Sources/HTMLKit/Framework/Environment/Negation.swift @@ -0,0 +1,23 @@ +/// A type thats represents an invert condition +@_documentation(visibility: internal) +public struct Negation: Conditionable { + + /// The left-hand side conditional + public let lhs: Conditionable +} + + +/// Creates a invert condition +/// +/// ```swift +/// Environment.when(!value) { +/// } +/// ``` +/// +/// - Parameters: +/// - lhs: The left-hand side conditional +/// +/// - Returns: A invert +public prefix func ! (lhs: Conditionable) -> Negation { + return Negation(lhs: lhs) +} diff --git a/Sources/HTMLKit/Framework/Rendering/Renderer.swift b/Sources/HTMLKit/Framework/Rendering/Renderer.swift index 1e410392..a7cf4d05 100644 --- a/Sources/HTMLKit/Framework/Rendering/Renderer.swift +++ b/Sources/HTMLKit/Framework/Rendering/Renderer.swift @@ -479,10 +479,33 @@ public final class Renderer { var result = false + if let value = statement.compound as? EnvironmentValue { + + guard let parent = self.environment.retrieve(for: value.parentPath) else { + throw Errors.environmentObjectNotFound + } + + guard let value = parent[keyPath: value.valuePath] else { + throw Errors.environmentValueNotFound + } + + guard let boolValue = value as? Bool else { + throw Errors.unableToCastEnvironmentValue + } + + if boolValue { + result = true + } + } + if let condition = statement.compound as? Condition { result = try render(condition: condition) } + if let negation = statement.compound as? Negation { + result = try render(negation: negation) + } + if let relation = statement.compound as? Relation { result = try render(relation: relation) } @@ -494,6 +517,30 @@ public final class Renderer { return try render(contents: statement.second) } + private func render(negation: Negation) throws -> Bool { + + if let value = negation.lhs as? EnvironmentValue { + + guard let parent = self.environment.retrieve(for: value.parentPath) else { + throw Errors.environmentObjectNotFound + } + + guard let value = parent[keyPath: value.valuePath] else { + throw Errors.environmentValueNotFound + } + + guard let boolValue = value as? Bool else { + throw Errors.unableToCastEnvironmentValue + } + + if !boolValue { + return true + } + } + + return false + } + private func render(relation: Relation) throws -> Bool { switch relation.term { diff --git a/Tests/HTMLKitTests/EnvironmentTests.swift b/Tests/HTMLKitTests/EnvironmentTests.swift index b5161346..eb1b7f8b 100644 --- a/Tests/HTMLKitTests/EnvironmentTests.swift +++ b/Tests/HTMLKitTests/EnvironmentTests.swift @@ -82,6 +82,7 @@ final class EnvironmentTests: XCTestCase { let firstName: String = "Jane" let lastName: String = "Doe" let age: Int = 40 + let loggedIn: Bool = true } struct TestView: View { @@ -92,6 +93,18 @@ final class EnvironmentTests: XCTestCase { var body: Content { Paragraph { + // Should return true + Environment.when(object.loggedIn) { + "True" + } + + // Should return false + Environment.when(!object.loggedIn) { + "True" + } then: { + "False" + } + // Should return false Environment.when(object.firstName == "John") { "True" @@ -146,7 +159,7 @@ final class EnvironmentTests: XCTestCase { XCTAssertEqual(try renderer.render(view: TestView()), """ -

FalseTrueTrueFalseFalseTrueTrueFalse

+

TrueFalseFalseTrueTrueFalseFalseTrueTrueFalse

""" ) } From 144cc226d50f50085673d8ad0a409b9081e5e6c1 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Wed, 8 Jan 2025 21:08:46 +0100 Subject: [PATCH 09/15] Refactor the code a bit --- .../Framework/Environment/Condition.swift | 6 +- .../Framework/Environment/Environment.swift | 198 +++++++++++++++ .../Environment/EnvironmentModifier.swift | 6 +- .../Environment/EnvironmentValue.swift | 7 +- .../Framework/Environment/Negation.swift | 14 +- .../Framework/Environment/Relation.swift | 19 +- .../Framework/Environment/Sequence.swift | 7 +- .../Framework/Environment/Statement.swift | 8 +- .../Framework/Rendering/Renderer.swift | 239 ++++-------------- .../Extensions/Vapor+HTMLKit.swift | 4 +- Tests/HTMLKitTests/EnvironmentTests.swift | 34 +-- 11 files changed, 309 insertions(+), 233 deletions(-) diff --git a/Sources/HTMLKit/Framework/Environment/Condition.swift b/Sources/HTMLKit/Framework/Environment/Condition.swift index 1bbece69..781e78b4 100644 --- a/Sources/HTMLKit/Framework/Environment/Condition.swift +++ b/Sources/HTMLKit/Framework/Environment/Condition.swift @@ -19,13 +19,13 @@ public struct Condition: Conditionable { } /// The left-hand side value - public let lhs: EnvironmentValue + internal let lhs: EnvironmentValue /// The right-hand side value to test against - public let rhs: any Comparable + internal let rhs: any Comparable /// The comparison to perfom - public let comparison: Comparison + internal let comparison: Comparison /// Initializes a condition /// diff --git a/Sources/HTMLKit/Framework/Environment/Environment.swift b/Sources/HTMLKit/Framework/Environment/Environment.swift index b8720657..f4b53149 100644 --- a/Sources/HTMLKit/Framework/Environment/Environment.swift +++ b/Sources/HTMLKit/Framework/Environment/Environment.swift @@ -3,8 +3,43 @@ import Foundation /// A class that represents the environment /// /// The environment provides storage for various settings used by the renderer +@_documentation(visibility: internal) public final class Environment { + /// An enumeration of possible rendering errors. + public enum Errors: Error { + + /// Indicates a casting error. + case unableToCastEnvironmentValue + + /// Indicates a wrong environment key. + case unindendedEnvironmentKey + + /// Indicates a missing environment object. + case environmentObjectNotFound + + /// Indicates a missing environment value. + case environmentValueNotFound + + /// A brief error description. + public var description: String { + + switch self { + case .unableToCastEnvironmentValue: + return "Unable to cast the environment value." + + case .unindendedEnvironmentKey: + return "The environment key is not indended." + + case .environmentValueNotFound: + return "Unable to retrieve environment value." + + case .environmentObjectNotFound: + return "Unable to retrieve environment object." + } + } + } + /// The storage of the environment private var storage: [AnyKeyPath: Any] @@ -63,6 +98,169 @@ public final class Environment { public func upsert(_ value: T, for path: AnyKeyPath) { storage[path] = value } + + /// Resolves an environment value + /// + /// - Parameter value: The environment value to resolve + /// + /// - Returns: The resolved environment value + internal func resolve(value: EnvironmentValue) throws -> Any { + + guard let parent = retrieve(for: value.parentPath) else { + throw Errors.environmentObjectNotFound + } + + guard let value = parent[keyPath: value.valuePath] else { + throw Errors.environmentValueNotFound + } + + return value + } + + /// Evaluates an environment value + /// + /// - Parameter value: The value to evaluate + /// + /// - Returns: The result of evaluation + internal func evaluate(value: EnvironmentValue) throws -> Bool { + + let value = try resolve(value: value) + + guard let boolValue = value as? Bool else { + throw Errors.unableToCastEnvironmentValue + } + + return boolValue + } + + /// Evaluates an environment relation + /// + /// - Parameter relation: The relation to evaluate + /// + /// - Returns: The result of the evaluation + internal func evaluate(relation: Relation) throws -> Bool { + + switch relation.term { + case .conjunction: + + var result = true + + if let condition = relation.lhs as? Condition { + result = try evaluate(condition: condition) + } + + if let relation = relation.lhs as? Relation { + result = try evaluate(relation: relation) + } + + if let negation = relation.lhs as? Negation { + result = try evaluate(negation: negation) + } + + if let value = relation.lhs as? EnvironmentValue { + result = try evaluate(value: value) + } + + if !result { + /// Bail early if the first result already is false + return result + } + + if let condition = relation.rhs as? Condition { + result = try evaluate(condition: condition) + } + + if let relation = relation.rhs as? Relation { + result = try evaluate(relation: relation) + } + + if let negation = relation.lhs as? Negation { + result = try evaluate(negation: negation) + } + + if let value = relation.lhs as? EnvironmentValue { + result = try evaluate(value: value) + } + + return result + + case .disjunction: + + var result = false + + if let condition = relation.lhs as? Condition { + result = try evaluate(condition: condition) + } + + if let relation = relation.lhs as? Relation { + result = try evaluate(relation: relation) + } + + if let negation = relation.lhs as? Negation { + result = try evaluate(negation: negation) + } + + if let value = relation.lhs as? EnvironmentValue { + result = try evaluate(value: value) + } + + if result { + /// Bail early if the first result is already true + return result + } + + if let condition = relation.rhs as? Condition { + result = try evaluate(condition: condition) + } + + if let relation = relation.rhs as? Relation { + result = try evaluate(relation: relation) + } + + if let negation = relation.lhs as? Negation { + result = try evaluate(negation: negation) + } + + if let value = relation.lhs as? EnvironmentValue { + result = try evaluate(value: value) + } + + return result + } + } + + /// Evaluates an environment condition + /// + /// - Parameter condition: The condition to evaluate + /// + /// - Returns: The result of the evaluation + internal func evaluate(condition: Condition) throws -> Bool { + + let value = try resolve(value: condition.lhs) + + switch condition.comparison { + case .equal: + return condition.rhs.equal(value) + + case .greater: + return condition.rhs.greater(value) + + case .unequal: + return condition.rhs.unequal(value) + + case .less: + return condition.rhs.less(value) + } + } + + /// Evaluates an environment negation + /// + /// - Parameter negation: The negation to evaluate + /// + /// - Returns: The result of the evaluation + internal func evaluate(negation: Negation) throws -> Bool { + return try !evaluate(value: negation.value) + } } extension Environment { diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift index cca4b822..46438175 100644 --- a/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift +++ b/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift @@ -5,13 +5,13 @@ public struct EnvironmentModifier: Content { /// The environment key - internal var key: AnyKeyPath + internal let key: AnyKeyPath /// The environment value - internal var value: Any? + internal let value: Any? /// The sub-content - internal var content: [Content] + internal let content: [Content] /// Initializes an environment modifier /// diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift index 5e36e159..8727046c 100644 --- a/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift +++ b/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift @@ -3,20 +3,21 @@ import Foundation /// A type that serves as a placeholder for an environment value /// /// The placeholder will be evaluated and resolved by the renderer when needed. +@_documentation(visibility: internal) public struct EnvironmentValue: Content { /// The path of the values parent - internal var parentPath: AnyKeyPath + internal let parentPath: AnyKeyPath /// The path of the value - internal var valuePath: AnyKeyPath + internal let valuePath: AnyKeyPath /// Initializes a environment value /// /// - Parameters: /// - parentPath: The key path of the parent /// - valuePath: The key path of the value - public init(parentPath: AnyKeyPath, valuePath: AnyKeyPath) { + internal init(parentPath: AnyKeyPath, valuePath: AnyKeyPath) { self.parentPath = parentPath self.valuePath = valuePath diff --git a/Sources/HTMLKit/Framework/Environment/Negation.swift b/Sources/HTMLKit/Framework/Environment/Negation.swift index 129d7f42..8d333073 100644 --- a/Sources/HTMLKit/Framework/Environment/Negation.swift +++ b/Sources/HTMLKit/Framework/Environment/Negation.swift @@ -3,7 +3,15 @@ public struct Negation: Conditionable { /// The left-hand side conditional - public let lhs: Conditionable + internal let value: EnvironmentValue + + /// Initializes the negation + /// + /// - Parameter lhs: The conditional to evaluate + public init(value: EnvironmentValue) { + + self.value = value + } } @@ -18,6 +26,6 @@ public struct Negation: Conditionable { /// - lhs: The left-hand side conditional /// /// - Returns: A invert -public prefix func ! (lhs: Conditionable) -> Negation { - return Negation(lhs: lhs) +public prefix func ! (value: EnvironmentValue) -> Negation { + return Negation(value: value) } diff --git a/Sources/HTMLKit/Framework/Environment/Relation.swift b/Sources/HTMLKit/Framework/Environment/Relation.swift index 10a1d0b6..78993fdc 100644 --- a/Sources/HTMLKit/Framework/Environment/Relation.swift +++ b/Sources/HTMLKit/Framework/Environment/Relation.swift @@ -17,13 +17,26 @@ public struct Relation: Conditionable { } /// The logical term specifying the relation - public let term: Term + internal let term: Term /// The left-hand side conditional - public let lhs: Conditionable + internal let lhs: Conditionable /// The right-hand side conditional - public let rhs: Conditionable + internal let rhs: Conditionable + + /// Initializes a relation + /// + /// - Parameters: + /// - term: The term on which the relation acts on + /// - lhs: The left-hand side conditional + /// - rhs: The right-hand side conditional to test against + public init(term: Term, lhs: Conditionable, rhs: Conditionable) { + + self.term = term + self.lhs = lhs + self.rhs = rhs + } } /// Creates a conjunctional relation between two conditionals diff --git a/Sources/HTMLKit/Framework/Environment/Sequence.swift b/Sources/HTMLKit/Framework/Environment/Sequence.swift index 800986e9..b2498712 100644 --- a/Sources/HTMLKit/Framework/Environment/Sequence.swift +++ b/Sources/HTMLKit/Framework/Environment/Sequence.swift @@ -1,18 +1,19 @@ /// A type representing a loop rendering content within the environment +@_documentation(visibility: internal) public struct Sequence: Content { /// The environment value of the sequence - let value: EnvironmentValue + internal let value: EnvironmentValue /// The accumulated content - let content: [Content] + internal let content: [Content] /// Initializes a loop /// /// - Parameters: /// - value: The environment value to retrieve the sequence from /// - content: The content to render for each item in the sequence - init(value: EnvironmentValue, content: [Content]) { + public init(value: EnvironmentValue, content: [Content]) { self.value = value self.content = content diff --git a/Sources/HTMLKit/Framework/Environment/Statement.swift b/Sources/HTMLKit/Framework/Environment/Statement.swift index e58d5b1a..09c6c1e1 100644 --- a/Sources/HTMLKit/Framework/Environment/Statement.swift +++ b/Sources/HTMLKit/Framework/Environment/Statement.swift @@ -3,13 +3,13 @@ public struct Statement: Content { /// The compound condition - let compound: Conditionable + internal let compound: Conditionable /// The first statement - let first: [Content] + internal let first: [Content] /// The second statement - let second: [Content] + internal let second: [Content] /// Initializes a statement /// @@ -17,7 +17,7 @@ public struct Statement: Content { /// - compound: The compound of conditionals /// - first: The statement to execute if conditionals are true /// - second: The statement to execute if conditionals are false - init(compound: Conditionable, first: [Content], second: [Content]) { + public init(compound: Conditionable, first: [Content], second: [Content]) { self.compound = compound self.first = first diff --git a/Sources/HTMLKit/Framework/Rendering/Renderer.swift b/Sources/HTMLKit/Framework/Rendering/Renderer.swift index a7cf4d05..eacde90a 100644 --- a/Sources/HTMLKit/Framework/Rendering/Renderer.swift +++ b/Sources/HTMLKit/Framework/Rendering/Renderer.swift @@ -9,40 +9,6 @@ import Logging @_documentation(visibility: internal) public final class Renderer { - - /// An enumeration of possible rendering errors. - public enum Errors: Error { - - /// Indicates a casting error. - case unableToCastEnvironmentValue - - /// Indicates a wrong environment key. - case unindendedEnvironmentKey - - /// Indicates a missing environment object. - case environmentObjectNotFound - - /// Indicates a missing environment value. - case environmentValueNotFound - - /// A brief error description. - public var description: String { - - switch self { - case .unableToCastEnvironmentValue: - return "Unable to cast the environment value." - - case .unindendedEnvironmentKey: - return "The environment key is not indended." - - case .environmentValueNotFound: - return "Unable to retrieve environment value." - - case .environmentObjectNotFound: - return "Unable to retrieve environment object." - } - } - } /// The context environment private var environment: Environment @@ -137,10 +103,7 @@ public final class Renderer { } if let statement = content as? Statement { - - if let yield = try render(statement: statement) { - result += yield - } + result += try render(statement: statement) } if let loop = content as? Sequence { @@ -231,10 +194,7 @@ public final class Renderer { } if let statement = content as? Statement { - - if let yield = try render(statement: statement) { - result += yield - } + result += try render(statement: statement) } if let string = content as? MarkdownString { @@ -360,10 +320,7 @@ public final class Renderer { } if let statement = content as? Statement { - - if let yield = try render(statement: statement) { - result += yield - } + result += try render(statement: statement) } if let loop = content as? Sequence { @@ -439,13 +396,7 @@ public final class Renderer { /// Renders a environment value. private func render(value: EnvironmentValue) throws -> String { - guard let parent = self.environment.retrieve(for: value.parentPath) else { - throw Errors.environmentObjectNotFound - } - - guard let value = parent[keyPath: value.valuePath] else { - throw Errors.environmentValueNotFound - } + let value = try self.environment.resolve(value: value) switch value { case let floatValue as Float: @@ -470,44 +421,29 @@ public final class Renderer { return formatter.string(from: dateValue) default: - throw Errors.unableToCastEnvironmentValue + throw Environment.Errors.unableToCastEnvironmentValue } } /// Renders a environment statement - private func render(statement: Statement) throws -> String? { + private func render(statement: Statement) throws -> String { var result = false - if let value = statement.compound as? EnvironmentValue { - - guard let parent = self.environment.retrieve(for: value.parentPath) else { - throw Errors.environmentObjectNotFound - } - - guard let value = parent[keyPath: value.valuePath] else { - throw Errors.environmentValueNotFound - } - - guard let boolValue = value as? Bool else { - throw Errors.unableToCastEnvironmentValue - } - - if boolValue { - result = true - } - } - if let condition = statement.compound as? Condition { - result = try render(condition: condition) + result = try environment.evaluate(condition: condition) } if let negation = statement.compound as? Negation { - result = try render(negation: negation) + result = try environment.evaluate(negation: negation) } if let relation = statement.compound as? Relation { - result = try render(relation: relation) + result = try environment.evaluate(relation: relation) + } + + if let value = statement.compound as? EnvironmentValue { + result = try environment.evaluate(value: value) } if result { @@ -517,114 +453,6 @@ public final class Renderer { return try render(contents: statement.second) } - private func render(negation: Negation) throws -> Bool { - - if let value = negation.lhs as? EnvironmentValue { - - guard let parent = self.environment.retrieve(for: value.parentPath) else { - throw Errors.environmentObjectNotFound - } - - guard let value = parent[keyPath: value.valuePath] else { - throw Errors.environmentValueNotFound - } - - guard let boolValue = value as? Bool else { - throw Errors.unableToCastEnvironmentValue - } - - if !boolValue { - return true - } - } - - return false - } - - private func render(relation: Relation) throws -> Bool { - - switch relation.term { - case .conjunction: - - var result = true - - if let condition = relation.lhs as? Condition { - result = try render(condition: condition) - } - - if let relation = relation.lhs as? Relation { - result = try render(relation: relation) - } - - if !result { - /// Bail early if the first result already is false - return result - } - - if let condition = relation.rhs as? Condition { - result = try render(condition: condition) - } - - if let relation = relation.rhs as? Relation { - result = try render(relation: relation) - } - - return result - - case .disjunction: - - var result = false - - if let condition = relation.lhs as? Condition { - result = try render(condition: condition) - } - - if let relation = relation.lhs as? Relation { - result = try render(relation: relation) - } - - if result { - /// Bail early if the first result is already true - return result - } - - if let condition = relation.rhs as? Condition { - result = try render(condition: condition) - } - - if let relation = relation.rhs as? Relation { - result = try render(relation: relation) - } - - return result - } - } - - private func render(condition: Condition) throws -> Bool { - - guard let parent = self.environment.retrieve(for: condition.lhs.parentPath) else { - throw Errors.environmentObjectNotFound - } - - guard let value = parent[keyPath: condition.lhs.valuePath] else { - throw Errors.environmentValueNotFound - } - - switch condition.comparison { - case .equal: - return condition.rhs.equal(value) - - case .greater: - return condition.rhs.greater(value) - - case .unequal: - return condition.rhs.unequal(value) - - case .less: - return condition.rhs.less(value) - } - } - /// Renders the node attributes. private func render(attributes: OrderedDictionary) throws -> String { @@ -680,25 +508,34 @@ public final class Renderer { return value } + /// Renders an environment loop + /// + /// - Parameter loop: The loop to resolve + /// + /// - Returns: The rendered loop private func render(loop: Sequence) throws -> String { - guard let parent = self.environment.retrieve(for: loop.value.parentPath) else { - throw Errors.environmentObjectNotFound - } - - guard let values = parent[keyPath: loop.value.valuePath] as? (any Swift.Sequence) else { - throw Errors.environmentValueNotFound + let value = try environment.resolve(value: loop.value) + + guard let sequence = value as? (any Swift.Sequence) else { + throw Environment.Errors.unableToCastEnvironmentValue } var result = "" - for value in values { + for value in sequence { try render(loop: loop.content, with: value, on: &result) } return result } + /// Renders the content within an environment loop + /// + /// - Parameters: + /// - contents: The content to render + /// - value: The value to resolve the environment value with + /// - result: The rendered content private func render(loop contents: [Content], with value: Any, on result: inout String) throws { for content in contents { @@ -763,6 +600,12 @@ public final class Renderer { } } + /// Renders a content element within an environment loop + /// + /// - Parameters: + /// - element: The element to render + /// - value: The value to resolve the environment value with + /// - result: The result private func render(loop element: some ContentNode, with value: Any, on result: inout String) throws { result += "<\(element.name)" @@ -780,6 +623,12 @@ public final class Renderer { result += "" } + /// Renders a custom element within an environment loop + /// + /// - Parameters: + /// - element: The element to render + /// - value: The value to resolve the environment value with + /// - result: The rendered content private func render(loop element: some CustomNode, with value: Any, on result: inout String) throws { result += "<\(element.name)" @@ -797,6 +646,12 @@ public final class Renderer { result += "" } + /// Renders an environment string within a environment loop + /// + /// - Parameters: + /// - envstring: The environment string to render + /// - value: The raw value to resolve the environment value with + /// - result: The result private func render(loop envstring: EnvironmentString, with value: Any, on result: inout String) throws { try render(loop: envstring.values, with: value, on: &result) } diff --git a/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift b/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift index b768eb40..311adc2c 100644 --- a/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift +++ b/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift @@ -119,7 +119,7 @@ extension Request { } } -extension HTMLKit.Renderer.Errors: AbortError { +extension HTMLKit.Environment.Errors: AbortError { @_implements(AbortError, reason) public var abortReason: String { self.description } @@ -127,7 +127,7 @@ extension HTMLKit.Renderer.Errors: AbortError { public var status: HTTPResponseStatus { .internalServerError } } -extension HTMLKit.Renderer.Errors: DebuggableError { +extension HTMLKit.Environment.Errors: DebuggableError { @_implements(DebuggableError, reason) public var debuggableReason: String { self.description } diff --git a/Tests/HTMLKitTests/EnvironmentTests.swift b/Tests/HTMLKitTests/EnvironmentTests.swift index eb1b7f8b..15c0cefa 100644 --- a/Tests/HTMLKitTests/EnvironmentTests.swift +++ b/Tests/HTMLKitTests/EnvironmentTests.swift @@ -12,14 +12,14 @@ final class EnvironmentTests: XCTestCase { struct FamilyObject: ViewModel { - let name: String = "Doe" - let father: FatherObject = FatherObject() + let name = "Doe" + let father = FatherObject() } struct FatherObject: ViewModel { - let avatar: String = "john_doe.jpeg" - let name: String = "John" + let avatar = "john_doe.jpeg" + let name = "John" } struct ParentView: View { @@ -79,10 +79,10 @@ final class EnvironmentTests: XCTestCase { struct TestObject: ViewModel { - let firstName: String = "Jane" - let lastName: String = "Doe" - let age: Int = 40 - let loggedIn: Bool = true + let firstName = "Jane" + let lastName = "Doe" + let age = 40 + let loggedIn = true } struct TestView: View { @@ -169,9 +169,9 @@ final class EnvironmentTests: XCTestCase { struct TestObject: ViewModel { - let firstName: String = "Jane" - let lastName: String = "Doe" - let age: Int = 40 + let firstName = "Jane" + let lastName = "Doe" + let age = 40 } struct TestView: View { @@ -226,9 +226,9 @@ final class EnvironmentTests: XCTestCase { struct TestObject: ViewModel { - let firstName: String = "Jane" - let lastName: String = "Doe" - let age: Int = 40 + let firstName = "Jane" + let lastName = "Doe" + let age = 40 } struct TestView: View { @@ -319,8 +319,8 @@ final class EnvironmentTests: XCTestCase { struct TestObject: ViewModel { - let name: String = "Jane" - let children: [String] = ["Janek", "Janet"] + let name = "Jane" + let children = ["Janek", "Janet"] } struct TestView: View { @@ -357,7 +357,7 @@ final class EnvironmentTests: XCTestCase { struct TestObject: ViewModel { - let name: String = "Jane" + let name = "Jane" } struct TestView: View { From 801b1ed77dfc207610a56fe81bc38e49517eac21 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Wed, 8 Jan 2025 21:28:47 +0100 Subject: [PATCH 10/15] Avoid type casting by using a type erasure --- .../Framework/Environment/Condition.swift | 4 +- .../Framework/Environment/Conditionable.swift | 4 - .../Framework/Environment/Conditional.swift | 16 ++++ .../Framework/Environment/Environment.swift | 93 ++++++++++--------- .../Environment/EnvironmentValue.swift | 18 ++-- .../Framework/Environment/Negation.swift | 13 ++- .../Framework/Environment/Relation.swift | 83 +++++++++++++++-- .../Framework/Environment/Statement.swift | 4 +- .../Framework/Rendering/Renderer.swift | 24 ++--- 9 files changed, 170 insertions(+), 89 deletions(-) delete mode 100644 Sources/HTMLKit/Framework/Environment/Conditionable.swift create mode 100644 Sources/HTMLKit/Framework/Environment/Conditional.swift diff --git a/Sources/HTMLKit/Framework/Environment/Condition.swift b/Sources/HTMLKit/Framework/Environment/Condition.swift index 781e78b4..2fa8c671 100644 --- a/Sources/HTMLKit/Framework/Environment/Condition.swift +++ b/Sources/HTMLKit/Framework/Environment/Condition.swift @@ -1,6 +1,6 @@ -/// A type representing a condition that compares an environment value against another value +/// A type representing a conditional that compares an environment value against another value @_documentation(visibility: internal) -public struct Condition: Conditionable { +public struct Condition { /// A enumeration of potential comparison public enum Comparison { diff --git a/Sources/HTMLKit/Framework/Environment/Conditionable.swift b/Sources/HTMLKit/Framework/Environment/Conditionable.swift deleted file mode 100644 index 772eb7d3..00000000 --- a/Sources/HTMLKit/Framework/Environment/Conditionable.swift +++ /dev/null @@ -1,4 +0,0 @@ -/// A type that defines a conditonal value which will be evualuated by the renderer. -@_documentation(visibility: internal) -public protocol Conditionable { -} diff --git a/Sources/HTMLKit/Framework/Environment/Conditional.swift b/Sources/HTMLKit/Framework/Environment/Conditional.swift new file mode 100644 index 00000000..6b7957c2 --- /dev/null +++ b/Sources/HTMLKit/Framework/Environment/Conditional.swift @@ -0,0 +1,16 @@ +/// A type that defines a conditonal value which will be evualuated by the renderer. +@_documentation(visibility: internal) +public indirect enum Conditional { + + /// Holds an relation + case relation(Relation) + + /// Holds a condition + case condition(Condition) + + /// Holds a negation + case negation(Negation) + + /// Holds a value + case value(EnvironmentValue) +} diff --git a/Sources/HTMLKit/Framework/Environment/Environment.swift b/Sources/HTMLKit/Framework/Environment/Environment.swift index f4b53149..91a12605 100644 --- a/Sources/HTMLKit/Framework/Environment/Environment.swift +++ b/Sources/HTMLKit/Framework/Environment/Environment.swift @@ -145,40 +145,36 @@ public final class Environment { var result = true - if let condition = relation.lhs as? Condition { + switch relation.lhs { + case .condition(let condition): result = try evaluate(condition: condition) - } - - if let relation = relation.lhs as? Relation { + + case .relation(let relation): result = try evaluate(relation: relation) - } - - if let negation = relation.lhs as? Negation { + + case .negation(let negation): result = try evaluate(negation: negation) - } - - if let value = relation.lhs as? EnvironmentValue { + + case .value(let value): result = try evaluate(value: value) } if !result { - /// Bail early if the first result already is false + // Bail early if the first result already is false return result } - - if let condition = relation.rhs as? Condition { - result = try evaluate(condition: condition) - } - if let relation = relation.rhs as? Relation { + switch relation.rhs { + case .condition(let condition): + result = try evaluate(condition: condition) + + case .relation(let relation): result = try evaluate(relation: relation) - } - - if let negation = relation.lhs as? Negation { + + case .negation(let negation): result = try evaluate(negation: negation) - } - - if let value = relation.lhs as? EnvironmentValue { + + case .value(let value): result = try evaluate(value: value) } @@ -188,40 +184,36 @@ public final class Environment { var result = false - if let condition = relation.lhs as? Condition { + switch relation.lhs { + case .condition(let condition): result = try evaluate(condition: condition) - } - - if let relation = relation.lhs as? Relation { + + case .relation(let relation): result = try evaluate(relation: relation) - } - - if let negation = relation.lhs as? Negation { + + case .negation(let negation): result = try evaluate(negation: negation) - } - - if let value = relation.lhs as? EnvironmentValue { + + case .value(let value): result = try evaluate(value: value) } if result { - /// Bail early if the first result is already true + // Bail early if the first result is already true return result } - if let condition = relation.rhs as? Condition { + switch relation.rhs { + case .condition(let condition): result = try evaluate(condition: condition) - } - - if let relation = relation.rhs as? Relation { + + case .relation(let relation): result = try evaluate(relation: relation) - } - - if let negation = relation.lhs as? Negation { + + case .negation(let negation): result = try evaluate(negation: negation) - } - - if let value = relation.lhs as? EnvironmentValue { + + case .value(let value): result = try evaluate(value: value) } @@ -272,7 +264,18 @@ extension Environment { /// - content: The content for the true statement /// /// - Returns: A environment condition - public static func when(_ condition: Conditionable, @ContentBuilder content: () -> [Content]) -> Statement { + public static func when(_ condition: EnvironmentValue, @ContentBuilder content: () -> [Content]) -> Statement { + return Statement(compound: .value(condition), first: content(), second: []) + } + + /// Evaluates one condition + /// + /// - Parameters: + /// - condition: The condition to evaluate + /// - content: The content for the true statement + /// + /// - Returns: A environment condition + public static func when(_ condition: Conditional, @ContentBuilder content: () -> [Content]) -> Statement { return Statement(compound: condition, first: content(), second: []) } @@ -284,7 +287,7 @@ extension Environment { /// - then: The content for the false statement /// /// - Returns: A environment condition - public static func when(_ condition: Conditionable, @ContentBuilder content: () -> [Content], @ContentBuilder then: () -> [Content]) -> Statement { + public static func when(_ condition: Conditional, @ContentBuilder content: () -> [Content], @ContentBuilder then: () -> [Content]) -> Statement { return Statement(compound: condition, first: content(), second: then()) } diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift index 8727046c..06afbc0f 100644 --- a/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift +++ b/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift @@ -24,7 +24,7 @@ public struct EnvironmentValue: Content { } } -extension EnvironmentValue: Conditionable { +extension EnvironmentValue { /// Concat environment value with environment value public static func + (lhs: Content, rhs: Self) -> Content { @@ -34,28 +34,28 @@ extension EnvironmentValue: Conditionable { /// Compare an environment value with another comparable value /// /// Makes an unequal evaluation - public static func != (lhs: Self, rhs: some Comparable) -> Condition { - return Condition(lhs: lhs, rhs: rhs, comparison: .unequal) + public static func != (lhs: Self, rhs: some Comparable) -> Conditional { + return .condition(Condition(lhs: lhs, rhs: rhs, comparison: .unequal)) } /// Compare an environment value with another comparable value /// /// Makes an equal evaluation - public static func == (lhs: Self, rhs: some Comparable) -> Condition { - return Condition(lhs: lhs, rhs: rhs, comparison: .equal) + public static func == (lhs: Self, rhs: some Comparable) -> Conditional { + return .condition(Condition(lhs: lhs, rhs: rhs, comparison: .equal)) } /// Compare an environment value with another comparable value /// /// Makes an less than evaluation - public static func < (lhs: Self, rhs: some Comparable) -> Condition { - return Condition(lhs: lhs, rhs: rhs, comparison: .less) + public static func < (lhs: Self, rhs: some Comparable) -> Conditional { + return .condition(Condition(lhs: lhs, rhs: rhs, comparison: .less)) } /// Compare an environment value with another comparable value /// /// Makes an greater than evaluation - public static func > (lhs: Self, rhs: some Comparable) -> Condition { - return Condition(lhs: lhs, rhs: rhs, comparison: .greater) + public static func > (lhs: Self, rhs: some Comparable) -> Conditional { + return .condition(Condition(lhs: lhs, rhs: rhs, comparison: .greater)) } } diff --git a/Sources/HTMLKit/Framework/Environment/Negation.swift b/Sources/HTMLKit/Framework/Environment/Negation.swift index 8d333073..042cad6f 100644 --- a/Sources/HTMLKit/Framework/Environment/Negation.swift +++ b/Sources/HTMLKit/Framework/Environment/Negation.swift @@ -1,6 +1,6 @@ -/// A type thats represents an invert condition +/// A type thats represents an invert conditional @_documentation(visibility: internal) -public struct Negation: Conditionable { +public struct Negation { /// The left-hand side conditional internal let value: EnvironmentValue @@ -14,8 +14,7 @@ public struct Negation: Conditionable { } } - -/// Creates a invert condition +/// Creates a invert conditional /// /// ```swift /// Environment.when(!value) { @@ -25,7 +24,7 @@ public struct Negation: Conditionable { /// - Parameters: /// - lhs: The left-hand side conditional /// -/// - Returns: A invert -public prefix func ! (value: EnvironmentValue) -> Negation { - return Negation(value: value) +/// - Returns: A invert conditional +public prefix func ! (value: EnvironmentValue) -> Conditional { + return .negation(Negation(value: value)) } diff --git a/Sources/HTMLKit/Framework/Environment/Relation.swift b/Sources/HTMLKit/Framework/Environment/Relation.swift index 78993fdc..fc78dc44 100644 --- a/Sources/HTMLKit/Framework/Environment/Relation.swift +++ b/Sources/HTMLKit/Framework/Environment/Relation.swift @@ -1,6 +1,6 @@ /// A type representing the logical relation between two conditionals @_documentation(visibility: internal) -public struct Relation: Conditionable { +public struct Relation { /// A enumeration of potential logical terms public enum Term { @@ -20,10 +20,10 @@ public struct Relation: Conditionable { internal let term: Term /// The left-hand side conditional - internal let lhs: Conditionable + internal let lhs: Conditional /// The right-hand side conditional - internal let rhs: Conditionable + internal let rhs: Conditional /// Initializes a relation /// @@ -31,7 +31,7 @@ public struct Relation: Conditionable { /// - term: The term on which the relation acts on /// - lhs: The left-hand side conditional /// - rhs: The right-hand side conditional to test against - public init(term: Term, lhs: Conditionable, rhs: Conditionable) { + public init(term: Term, lhs: Conditional, rhs: Conditional) { self.term = term self.lhs = lhs @@ -40,7 +40,40 @@ public struct Relation: Conditionable { } /// Creates a conjunctional relation between two conditionals -/// +/// +/// ```swift +/// Environment.when(value > 0 && value) { +/// } +/// ``` +/// +/// - Parameters: +/// - lhs: The left-hand side conditional +/// - rhs: An environment value +/// +/// - Returns: A conjunctional relation +public func && (lhs: Conditional, rhs: EnvironmentValue) -> Conditional { + return .relation(Relation(term: .conjunction, lhs: lhs, rhs: .value(rhs))) +} + +/// Creates a conjunctional relation between two conditionals +/// +/// ```swift +/// Environment.when(value && value > 0) { +/// } +/// ``` +/// +/// - Parameters: +/// - lhs: The left-hand side conditional +/// - rhs: An environment value +/// +/// - Returns: A conjunctional relation +public func && (lhs: EnvironmentValue, rhs: Conditional) -> Conditional { + return .relation(Relation(term: .conjunction, lhs: .value(lhs), rhs: rhs)) +} + + +/// Creates a conjunctional relation between two conditionals +/// /// ```swift /// Environment.when(value > 0 && value < 2) { /// } @@ -51,8 +84,40 @@ public struct Relation: Conditionable { /// - rhs: The right-hand side conditional /// /// - Returns: A conjunctional relation -public func && (lhs: Conditionable, rhs: Conditionable) -> Relation { - return Relation(term: .conjunction, lhs: lhs, rhs: rhs) +public func && (lhs: Conditional, rhs: Conditional) -> Conditional { + return .relation(Relation(term: .conjunction, lhs: lhs, rhs: rhs)) +} + +/// Creates a disjunctional relation between two conditionals +/// +/// ```swift +/// Environment.when(value > 0 || value) { +/// } +/// ``` +/// +/// - Parameters: +/// - lhs: The left-hand side conditional +/// - rhs: The right-hand side conditional +/// +/// - Returns: A disjunctional relation +public func || (lhs: Conditional, rhs: EnvironmentValue) -> Conditional { + return .relation(Relation(term: .disjunction, lhs: lhs, rhs: .value(rhs))) +} + +/// Creates a disjunctional relation between two conditionals +/// +/// ```swift +/// Environment.when(value || value > 0) { +/// } +/// ``` +/// +/// - Parameters: +/// - lhs: The left-hand side conditional +/// - rhs: The right-hand side conditional +/// +/// - Returns: A disjunctional relation +public func || (lhs: EnvironmentValue, rhs: Conditional) -> Conditional { + return .relation(Relation(term: .disjunction, lhs: .value(lhs), rhs: rhs)) } /// Creates a disjunctional relation between two conditionals @@ -67,7 +132,7 @@ public func && (lhs: Conditionable, rhs: Conditionable) -> Relation { /// - rhs: The right-hand side conditional /// /// - Returns: A disjunctional relation -public func || (lhs: Conditionable, rhs: Conditionable) -> Relation { - return Relation(term: .disjunction, lhs: lhs, rhs: rhs) +public func || (lhs: Conditional, rhs: Conditional) -> Conditional { + return .relation(Relation(term: .disjunction, lhs: lhs, rhs: rhs)) } diff --git a/Sources/HTMLKit/Framework/Environment/Statement.swift b/Sources/HTMLKit/Framework/Environment/Statement.swift index 09c6c1e1..3a6888f3 100644 --- a/Sources/HTMLKit/Framework/Environment/Statement.swift +++ b/Sources/HTMLKit/Framework/Environment/Statement.swift @@ -3,7 +3,7 @@ public struct Statement: Content { /// The compound condition - internal let compound: Conditionable + internal let compound: Conditional /// The first statement internal let first: [Content] @@ -17,7 +17,7 @@ public struct Statement: Content { /// - compound: The compound of conditionals /// - first: The statement to execute if conditionals are true /// - second: The statement to execute if conditionals are false - public init(compound: Conditionable, first: [Content], second: [Content]) { + public init(compound: Conditional, first: [Content], second: [Content]) { self.compound = compound self.first = first diff --git a/Sources/HTMLKit/Framework/Rendering/Renderer.swift b/Sources/HTMLKit/Framework/Rendering/Renderer.swift index eacde90a..b6cefa8f 100644 --- a/Sources/HTMLKit/Framework/Rendering/Renderer.swift +++ b/Sources/HTMLKit/Framework/Rendering/Renderer.swift @@ -426,26 +426,28 @@ public final class Renderer { } /// Renders a environment statement + /// + /// - Parameter statement: The statement to resolve + /// + /// - Returns: The rendered condition private func render(statement: Statement) throws -> String { var result = false - if let condition = statement.compound as? Condition { + switch statement.compound { + case .value(let value): + result = try environment.evaluate(value: value) + + case .condition(let condition): result = try environment.evaluate(condition: condition) - } - - if let negation = statement.compound as? Negation { + + case .negation(let negation): result = try environment.evaluate(negation: negation) - } - - if let relation = statement.compound as? Relation { + + case .relation(let relation): result = try environment.evaluate(relation: relation) } - if let value = statement.compound as? EnvironmentValue { - result = try environment.evaluate(value: value) - } - if result { return try render(contents: statement.first) } From 3bb47e391169180c70162335af59816b7151feed Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Wed, 8 Jan 2025 21:38:01 +0100 Subject: [PATCH 11/15] Test the error reporting for the environment through the provider --- Tests/HTMLKitVaporTests/ProviderTests.swift | 72 +++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/Tests/HTMLKitVaporTests/ProviderTests.swift b/Tests/HTMLKitVaporTests/ProviderTests.swift index a96a9fb3..2877188b 100644 --- a/Tests/HTMLKitVaporTests/ProviderTests.swift +++ b/Tests/HTMLKitVaporTests/ProviderTests.swift @@ -393,4 +393,76 @@ final class ProviderTests: XCTestCase { ) } } + + /// Tests the error reporting to Vapor for issues that may occur during environment access. + /// + /// The error is expected to be classified as an internal server error and includes a error message. + func testEnvironmentErrorReporting() throws { + + struct TestObject { + + let firstName = "Jane" + } + + struct UnkownObject: HTMLKit.View { + + @EnvironmentObject(TestObject.self) + var object + + var body: HTMLKit.Content { + Paragraph { + Environment.when(object.firstName) { + "True" + } + } + } + } + + struct WrongCast: HTMLKit.View { + + @EnvironmentObject(TestObject.self) + var object + + var body: HTMLKit.Content { + Paragraph { + Environment.when(object.firstName) { + "True" + } + } + .environment(object: TestObject()) + } + } + + let app = Application(.testing) + + defer { app.shutdown() } + + app.get("unkownobject") { request async throws -> Vapor.View in + + return try await request.htmlkit.render(UnkownObject()) + } + + app.get("wrongcast") { request async throws -> Vapor.View in + + return try await request.htmlkit.render(WrongCast()) + } + + try app.test(.GET, "unkownobject") { response in + + XCTAssertEqual(response.status, .internalServerError) + + let abort = try response.content.decode(AbortResponse.self) + + XCTAssertEqual(abort.reason, "Unable to retrieve environment object.") + } + + try app.test(.GET, "wrongcast") { response in + + XCTAssertEqual(response.status, .internalServerError) + + let abort = try response.content.decode(AbortResponse.self) + + XCTAssertEqual(abort.reason, "Unable to cast the environment value.") + } + } } From a86602c480df8664babf49e80d1b8c7248659567 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Fri, 10 Jan 2025 17:42:15 +0100 Subject: [PATCH 12/15] Make environment statement and sequence a global element --- Sources/HTMLKit/Framework/Environment/Sequence.swift | 2 +- Sources/HTMLKit/Framework/Environment/Statement.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/HTMLKit/Framework/Environment/Sequence.swift b/Sources/HTMLKit/Framework/Environment/Sequence.swift index b2498712..f591ec81 100644 --- a/Sources/HTMLKit/Framework/Environment/Sequence.swift +++ b/Sources/HTMLKit/Framework/Environment/Sequence.swift @@ -1,6 +1,6 @@ /// A type representing a loop rendering content within the environment @_documentation(visibility: internal) -public struct Sequence: Content { +public struct Sequence: GlobalElement { /// The environment value of the sequence internal let value: EnvironmentValue diff --git a/Sources/HTMLKit/Framework/Environment/Statement.swift b/Sources/HTMLKit/Framework/Environment/Statement.swift index 3a6888f3..2db7286e 100644 --- a/Sources/HTMLKit/Framework/Environment/Statement.swift +++ b/Sources/HTMLKit/Framework/Environment/Statement.swift @@ -1,6 +1,6 @@ /// A type representing a conditional block within the environment @_documentation(visibility: internal) -public struct Statement: Content { +public struct Statement: GlobalElement { /// The compound condition internal let compound: Conditional From 5e405c105cdcfb7a81584cad5479a66c82dada7c Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Fri, 10 Jan 2025 19:37:30 +0100 Subject: [PATCH 13/15] Introduce a method to unwrap optional environment values --- .../Framework/Environment/Conditional.swift | 3 + .../Framework/Environment/Environment.swift | 57 ++++++++++++++++++- .../Framework/Environment/Nullable.swift | 16 ++++++ .../Framework/Rendering/Renderer.swift | 3 + Tests/HTMLKitTests/EnvironmentTests.swift | 43 ++++++++++++++ 5 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 Sources/HTMLKit/Framework/Environment/Nullable.swift diff --git a/Sources/HTMLKit/Framework/Environment/Conditional.swift b/Sources/HTMLKit/Framework/Environment/Conditional.swift index 6b7957c2..d8904f7b 100644 --- a/Sources/HTMLKit/Framework/Environment/Conditional.swift +++ b/Sources/HTMLKit/Framework/Environment/Conditional.swift @@ -2,6 +2,9 @@ @_documentation(visibility: internal) public indirect enum Conditional { + /// Holds an optional + case optional(EnvironmentValue) + /// Holds an relation case relation(Relation) diff --git a/Sources/HTMLKit/Framework/Environment/Environment.swift b/Sources/HTMLKit/Framework/Environment/Environment.swift index 91a12605..d0f5e977 100644 --- a/Sources/HTMLKit/Framework/Environment/Environment.swift +++ b/Sources/HTMLKit/Framework/Environment/Environment.swift @@ -124,9 +124,7 @@ public final class Environment { /// - Returns: The result of evaluation internal func evaluate(value: EnvironmentValue) throws -> Bool { - let value = try resolve(value: value) - - guard let boolValue = value as? Bool else { + guard let boolValue = try resolve(value: value) as? Bool else { throw Errors.unableToCastEnvironmentValue } @@ -146,6 +144,9 @@ public final class Environment { var result = true switch relation.lhs { + case .optional(let optional): + result = try evaluate(optional: optional) + case .condition(let condition): result = try evaluate(condition: condition) @@ -165,6 +166,9 @@ public final class Environment { } switch relation.rhs { + case .optional(let optional): + result = try evaluate(optional: optional) + case .condition(let condition): result = try evaluate(condition: condition) @@ -185,6 +189,9 @@ public final class Environment { var result = false switch relation.lhs { + case .optional(let optional): + result = try evaluate(optional: optional) + case .condition(let condition): result = try evaluate(condition: condition) @@ -204,6 +211,9 @@ public final class Environment { } switch relation.rhs { + case .optional(let optional): + result = try evaluate(optional: optional) + case .condition(let condition): result = try evaluate(condition: condition) @@ -253,6 +263,24 @@ public final class Environment { internal func evaluate(negation: Negation) throws -> Bool { return try !evaluate(value: negation.value) } + + /// Evaluates an environment optional + /// + /// - Parameter optional: The optional to evaluate + /// + /// - Returns: The result of the evaluation + internal func evaluate(optional: EnvironmentValue) throws -> Bool { + + guard let optionalValue = try resolve(value: optional) as? Nullable else { + throw Errors.unableToCastEnvironmentValue + } + + if !optionalValue.isNull { + return true + } + + return false + } } extension Environment { @@ -279,6 +307,29 @@ extension Environment { return Statement(compound: condition, first: content(), second: []) } + /// Unwraps a optional environment value + /// + /// - Parameters: + /// - value: The optional value to unwrap + /// - content: The content for some statement + /// + /// - Returns: A environment statement + public static func unwrap(_ value: EnvironmentValue, @ContentBuilder content: (EnvironmentValue) -> [Content]) -> Statement { + return Statement(compound: .optional(value), first: content(value), second: []) + } + + /// Unwraps a optional environment value + /// + /// - Parameters: + /// - value: The optional value to unwrap + /// - content: The content for some statement + /// - then: The content for none statement + /// + /// - Returns: A environment statement + public static func unwrap(_ value: EnvironmentValue, @ContentBuilder content: (EnvironmentValue) -> [Content], @ContentBuilder then: () -> [Content]) -> Statement { + return Statement(compound: .optional(value), first: content(value), second: then()) + } + /// Evaluates one condition /// /// - Parameters: diff --git a/Sources/HTMLKit/Framework/Environment/Nullable.swift b/Sources/HTMLKit/Framework/Environment/Nullable.swift new file mode 100644 index 00000000..70fb35a1 --- /dev/null +++ b/Sources/HTMLKit/Framework/Environment/Nullable.swift @@ -0,0 +1,16 @@ +/// A type that represent +/// +/// > Note: This protocol is intended as a temporary workaround. +@_documentation(visibility: internal) +internal protocol Nullable { + + /// Checks whether the value is absent without needing to know the underlying type. + var isNull: Bool { get } +} + +extension Optional: Nullable { + + internal var isNull: Bool { + return self == nil + } +} diff --git a/Sources/HTMLKit/Framework/Rendering/Renderer.swift b/Sources/HTMLKit/Framework/Rendering/Renderer.swift index b6cefa8f..503f28c6 100644 --- a/Sources/HTMLKit/Framework/Rendering/Renderer.swift +++ b/Sources/HTMLKit/Framework/Rendering/Renderer.swift @@ -435,6 +435,9 @@ public final class Renderer { var result = false switch statement.compound { + case .optional(let optional): + result = try environment.evaluate(optional: optional) + case .value(let value): result = try environment.evaluate(value: value) diff --git a/Tests/HTMLKitTests/EnvironmentTests.swift b/Tests/HTMLKitTests/EnvironmentTests.swift index 15c0cefa..e49a47dc 100644 --- a/Tests/HTMLKitTests/EnvironmentTests.swift +++ b/Tests/HTMLKitTests/EnvironmentTests.swift @@ -350,6 +350,49 @@ final class EnvironmentTests: XCTestCase { ) } + /// Tests the conditional rendering based on an optional environment value + /// + /// The renderer is expected to evaluate the presence of the value and render the right content + /// accordingly. + func testEnvironmentUnwrap() throws { + + struct TestObject: ViewModel { + + let some: String? = "Some" + let none: String? = nil + } + + struct TestView: View { + + @EnvironmentObject(TestObject.self) + var object + + var body: Content { + Paragraph { + + // Should return none, cause the value is nil + Environment.unwrap(object.none) { value in + value + } then: { + "None" + } + + // Should return the value + Environment.unwrap(object.some) { value in + value + } + } + .environment(object: TestObject()) + } + } + + XCTAssertEqual(try renderer.render(view: TestView()), + """ +

NoneSome

+ """ + ) + } + /// Tests the string interpolation with an environment value /// /// The renderer is expected to render the string correctly From a65cd382b3dd575e1ae96e2bdc8acc863d2da9d0 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Fri, 10 Jan 2025 19:53:42 +0100 Subject: [PATCH 14/15] Tidy up a bit --- .../Framework/Environment/Environment.swift | 42 ++++++++++++------- .../Framework/Environment/Nullable.swift | 9 +--- .../Extensions/Comparable+HTMLKit.swift | 8 ++-- .../Extensions/Datatypes+Content.swift | 2 - .../Extensions/Optional+HTMLKit.swift | 9 ++++ Tests/HTMLKitTests/EnvironmentTests.swift | 8 ---- 6 files changed, 41 insertions(+), 37 deletions(-) create mode 100644 Sources/HTMLKit/Framework/Extensions/Optional+HTMLKit.swift diff --git a/Sources/HTMLKit/Framework/Environment/Environment.swift b/Sources/HTMLKit/Framework/Environment/Environment.swift index d0f5e977..b199294b 100644 --- a/Sources/HTMLKit/Framework/Environment/Environment.swift +++ b/Sources/HTMLKit/Framework/Environment/Environment.swift @@ -291,22 +291,46 @@ extension Environment { /// - condition: The condition to evaluate /// - content: The content for the true statement /// - /// - Returns: A environment condition + /// - Returns: A environment statement public static func when(_ condition: EnvironmentValue, @ContentBuilder content: () -> [Content]) -> Statement { return Statement(compound: .value(condition), first: content(), second: []) } + /// Evaluates one condition + /// + /// - Parameters: + /// - condition: The condition to evaluate + /// - content: The content for the true statement + /// - then: The content for the false statement + /// + /// - Returns: A environment statement + public static func when(_ condition: EnvironmentValue, @ContentBuilder content: () -> [Content], @ContentBuilder then: () -> [Content]) -> Statement { + return Statement(compound: .value(condition), first: content(), second: then()) + } + /// Evaluates one condition /// /// - Parameters: /// - condition: The condition to evaluate /// - content: The content for the true statement /// - /// - Returns: A environment condition + /// - Returns: A environment statement public static func when(_ condition: Conditional, @ContentBuilder content: () -> [Content]) -> Statement { return Statement(compound: condition, first: content(), second: []) } + /// Evaluates one condition + /// + /// - Parameters: + /// - condition: The condition to evaluate + /// - content: The content for the true statement + /// - then: The content for the false statement + /// + /// - Returns: A environment statement + public static func when(_ condition: Conditional, @ContentBuilder content: () -> [Content], @ContentBuilder then: () -> [Content]) -> Statement { + return Statement(compound: condition, first: content(), second: then()) + } + /// Unwraps a optional environment value /// /// - Parameters: @@ -330,25 +354,13 @@ extension Environment { return Statement(compound: .optional(value), first: content(value), second: then()) } - /// Evaluates one condition - /// - /// - Parameters: - /// - condition: The condition to evaluate - /// - content: The content for the true statement - /// - then: The content for the false statement - /// - /// - Returns: A environment condition - public static func when(_ condition: Conditional, @ContentBuilder content: () -> [Content], @ContentBuilder then: () -> [Content]) -> Statement { - return Statement(compound: condition, first: content(), second: then()) - } - /// Iterates through a sequence of values /// /// - Parameters: /// - sequence: The sequence to iterate over /// - content: The content for the iteration /// - /// - Returns: A environment condition + /// - Returns: A environment sequence public static func loop(_ sequence: EnvironmentValue, @ContentBuilder content: (EnvironmentValue) -> [Content]) -> Sequence { return Sequence(value: sequence, content: content(sequence)) } diff --git a/Sources/HTMLKit/Framework/Environment/Nullable.swift b/Sources/HTMLKit/Framework/Environment/Nullable.swift index 70fb35a1..4ef47815 100644 --- a/Sources/HTMLKit/Framework/Environment/Nullable.swift +++ b/Sources/HTMLKit/Framework/Environment/Nullable.swift @@ -1,4 +1,4 @@ -/// A type that represent +/// A type that represent a nullable value. /// /// > Note: This protocol is intended as a temporary workaround. @_documentation(visibility: internal) @@ -7,10 +7,3 @@ internal protocol Nullable { /// Checks whether the value is absent without needing to know the underlying type. var isNull: Bool { get } } - -extension Optional: Nullable { - - internal var isNull: Bool { - return self == nil - } -} diff --git a/Sources/HTMLKit/Framework/Extensions/Comparable+HTMLKit.swift b/Sources/HTMLKit/Framework/Extensions/Comparable+HTMLKit.swift index d3b6dde6..ef966e0f 100644 --- a/Sources/HTMLKit/Framework/Extensions/Comparable+HTMLKit.swift +++ b/Sources/HTMLKit/Framework/Extensions/Comparable+HTMLKit.swift @@ -6,7 +6,7 @@ extension Comparable { /// - other: The other value to compare /// /// - Returns: The result - public func equal(_ other: Any) -> Bool { + internal func equal(_ other: Any) -> Bool { guard let other = other as? Self else { return false @@ -21,7 +21,7 @@ extension Comparable { /// - other: The other value to compare /// /// - Returns: The result - public func unequal(_ other: Any) -> Bool { + internal func unequal(_ other: Any) -> Bool { return !equal(other) } @@ -31,7 +31,7 @@ extension Comparable { /// - other: The other value to compare /// /// - Returns: The result - public func greater(_ other: Any) -> Bool { + internal func greater(_ other: Any) -> Bool { guard let other = other as? Self else { return false @@ -46,7 +46,7 @@ extension Comparable { /// - other: The other value to compare /// /// - Returns: The result - public func less(_ other: Any) -> Bool { + internal func less(_ other: Any) -> Bool { return !greater(other) } } diff --git a/Sources/HTMLKit/Framework/Extensions/Datatypes+Content.swift b/Sources/HTMLKit/Framework/Extensions/Datatypes+Content.swift index b5444a21..b20c7978 100644 --- a/Sources/HTMLKit/Framework/Extensions/Datatypes+Content.swift +++ b/Sources/HTMLKit/Framework/Extensions/Datatypes+Content.swift @@ -15,8 +15,6 @@ extension Float: Content {} extension Int: Content {} -extension Optional: Content{} - extension String: Content { static public func + (lhs: Content, rhs: Self) -> Content { diff --git a/Sources/HTMLKit/Framework/Extensions/Optional+HTMLKit.swift b/Sources/HTMLKit/Framework/Extensions/Optional+HTMLKit.swift new file mode 100644 index 00000000..d03bc9c4 --- /dev/null +++ b/Sources/HTMLKit/Framework/Extensions/Optional+HTMLKit.swift @@ -0,0 +1,9 @@ +extension Optional: Nullable { + + internal var isNull: Bool { + return self == nil + } +} + +extension Optional: Content { +} diff --git a/Tests/HTMLKitTests/EnvironmentTests.swift b/Tests/HTMLKitTests/EnvironmentTests.swift index e49a47dc..f6c12dd0 100644 --- a/Tests/HTMLKitTests/EnvironmentTests.swift +++ b/Tests/HTMLKitTests/EnvironmentTests.swift @@ -80,7 +80,6 @@ final class EnvironmentTests: XCTestCase { struct TestObject: ViewModel { let firstName = "Jane" - let lastName = "Doe" let age = 40 let loggedIn = true } @@ -169,8 +168,6 @@ final class EnvironmentTests: XCTestCase { struct TestObject: ViewModel { - let firstName = "Jane" - let lastName = "Doe" let age = 40 } @@ -226,8 +223,6 @@ final class EnvironmentTests: XCTestCase { struct TestObject: ViewModel { - let firstName = "Jane" - let lastName = "Doe" let age = 40 } @@ -276,8 +271,6 @@ final class EnvironmentTests: XCTestCase { struct TestObject: ViewModel { - let firstName = "Jane" - let lastName = "Doe" let age = 40 } @@ -319,7 +312,6 @@ final class EnvironmentTests: XCTestCase { struct TestObject: ViewModel { - let name = "Jane" let children = ["Janek", "Janet"] } From 2b64212ff158ce1287766782a06716b7f8ba1fe7 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Fri, 10 Jan 2025 20:51:56 +0100 Subject: [PATCH 15/15] Rename the method for the conditional statement --- .../Framework/Environment/Environment.swift | 8 ++-- Tests/HTMLKitTests/EnvironmentTests.swift | 38 +++++++++---------- Tests/HTMLKitVaporTests/ProviderTests.swift | 4 +- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Sources/HTMLKit/Framework/Environment/Environment.swift b/Sources/HTMLKit/Framework/Environment/Environment.swift index b199294b..0e35029d 100644 --- a/Sources/HTMLKit/Framework/Environment/Environment.swift +++ b/Sources/HTMLKit/Framework/Environment/Environment.swift @@ -292,7 +292,7 @@ extension Environment { /// - content: The content for the true statement /// /// - Returns: A environment statement - public static func when(_ condition: EnvironmentValue, @ContentBuilder content: () -> [Content]) -> Statement { + public static func check(_ condition: EnvironmentValue, @ContentBuilder content: () -> [Content]) -> Statement { return Statement(compound: .value(condition), first: content(), second: []) } @@ -304,7 +304,7 @@ extension Environment { /// - then: The content for the false statement /// /// - Returns: A environment statement - public static func when(_ condition: EnvironmentValue, @ContentBuilder content: () -> [Content], @ContentBuilder then: () -> [Content]) -> Statement { + public static func check(_ condition: EnvironmentValue, @ContentBuilder content: () -> [Content], @ContentBuilder then: () -> [Content]) -> Statement { return Statement(compound: .value(condition), first: content(), second: then()) } @@ -315,7 +315,7 @@ extension Environment { /// - content: The content for the true statement /// /// - Returns: A environment statement - public static func when(_ condition: Conditional, @ContentBuilder content: () -> [Content]) -> Statement { + public static func check(_ condition: Conditional, @ContentBuilder content: () -> [Content]) -> Statement { return Statement(compound: condition, first: content(), second: []) } @@ -327,7 +327,7 @@ extension Environment { /// - then: The content for the false statement /// /// - Returns: A environment statement - public static func when(_ condition: Conditional, @ContentBuilder content: () -> [Content], @ContentBuilder then: () -> [Content]) -> Statement { + public static func check(_ condition: Conditional, @ContentBuilder content: () -> [Content], @ContentBuilder then: () -> [Content]) -> Statement { return Statement(compound: condition, first: content(), second: then()) } diff --git a/Tests/HTMLKitTests/EnvironmentTests.swift b/Tests/HTMLKitTests/EnvironmentTests.swift index f6c12dd0..700cee9a 100644 --- a/Tests/HTMLKitTests/EnvironmentTests.swift +++ b/Tests/HTMLKitTests/EnvironmentTests.swift @@ -93,60 +93,60 @@ final class EnvironmentTests: XCTestCase { Paragraph { // Should return true - Environment.when(object.loggedIn) { + Environment.check(object.loggedIn) { "True" } // Should return false - Environment.when(!object.loggedIn) { + Environment.check(!object.loggedIn) { "True" } then: { "False" } // Should return false - Environment.when(object.firstName == "John") { + Environment.check(object.firstName == "John") { "True" } then: { "False" } // The counter test, should return true - Environment.when(object.firstName == "Jane") { + Environment.check(object.firstName == "Jane") { "True" } // Should return true - Environment.when(object.firstName != "John") { + Environment.check(object.firstName != "John") { "True" } // The counter test, should return false - Environment.when(object.firstName != "Jane") { + Environment.check(object.firstName != "Jane") { "True" } then: { "False" } // Should return false - Environment.when(object.age > 41) { + Environment.check(object.age > 41) { "True" } then: { "False" } // The counter test, should return true - Environment.when(object.age > 39) { + Environment.check(object.age > 39) { "True" } // Should return true - Environment.when(object.age < 41) { + Environment.check(object.age < 41) { "True" } // The counter test, should return false - Environment.when(object.age < 39) { + Environment.check(object.age < 39) { "True" } then: { "False" @@ -180,28 +180,28 @@ final class EnvironmentTests: XCTestCase { Paragraph { // The relation is true, cause both conditions are true - Environment.when(object.age > 39 && object.age < 41) { + Environment.check(object.age > 39 && object.age < 41) { "True" } then: { "False" } // The relation is false, cause both conditions are false - Environment.when(object.age < 39 && object.age > 41) { + Environment.check(object.age < 39 && object.age > 41) { "True" } then: { "False" } // The relation is false, cause the first condition is false - Environment.when(object.age > 41 && object.age > 39) { + Environment.check(object.age > 41 && object.age > 39) { "True" } then: { "False" } // The relation is false, cause the second condition is false - Environment.when(object.age > 39 && object.age > 41) { + Environment.check(object.age > 39 && object.age > 41) { "True" } then: { "False" @@ -235,21 +235,21 @@ final class EnvironmentTests: XCTestCase { Paragraph { // The relation is true, cause the second condition is true - Environment.when(object.age > 41 || object.age == 40) { + Environment.check(object.age > 41 || object.age == 40) { "True" } then: { "False" } // The relation is true, cause the first condition is true - Environment.when(object.age == 40 || object.age > 41) { + Environment.check(object.age == 40 || object.age > 41) { "True" } then: { "False" } // The relation is false, cause all conditions are false - Environment.when(object.age == 50 || object.age > 41) { + Environment.check(object.age == 50 || object.age > 41) { "True" } then: { "False" @@ -283,14 +283,14 @@ final class EnvironmentTests: XCTestCase { Paragraph { // The statement is true, cause the first relation is true - Environment.when(object.age == 40 || object.age > 41 && object.age < 39) { + Environment.check(object.age == 40 || object.age > 41 && object.age < 39) { "True" } then: { "False" } // The statement is true, cause the second relation is true - Environment.when(object.age == 50 || object.age < 41 && object.age > 39) { + Environment.check(object.age == 50 || object.age < 41 && object.age > 39) { "True" } then: { "False" diff --git a/Tests/HTMLKitVaporTests/ProviderTests.swift b/Tests/HTMLKitVaporTests/ProviderTests.swift index 2877188b..5940ebcc 100644 --- a/Tests/HTMLKitVaporTests/ProviderTests.swift +++ b/Tests/HTMLKitVaporTests/ProviderTests.swift @@ -411,7 +411,7 @@ final class ProviderTests: XCTestCase { var body: HTMLKit.Content { Paragraph { - Environment.when(object.firstName) { + Environment.check(object.firstName) { "True" } } @@ -425,7 +425,7 @@ final class ProviderTests: XCTestCase { var body: HTMLKit.Content { Paragraph { - Environment.when(object.firstName) { + Environment.check(object.firstName) { "True" } }