diff --git a/CHANGELOG.md b/CHANGELOG.md index d0a4527..a8663b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ _None_ - Made the `Template.render(_:)` method (that accepts a `Context`) public. [David Jennes](https://github.com/djbe) [#322](https://github.com/stencilproject/Stencil/pull/322) +- Enable dynamic member lookup using a new `DynamicMemberLookup` protocol. Conform your own types to this protocol to support dynamic member from with contexts. + [Ilya Puchka](https://github.com/ilyapuchka) + [#219](https://github.com/stencilproject/Stencil/issues/219) + [#246](https://github.com/stencilproject/Stencil/pull/246) ### Deprecations diff --git a/Sources/DynamicMemberLookup.swift b/Sources/DynamicMemberLookup.swift new file mode 100644 index 0000000..47aae01 --- /dev/null +++ b/Sources/DynamicMemberLookup.swift @@ -0,0 +1,15 @@ +/// Marker protocol so we can know which types support `@dynamicMemberLookup`. Add this to your own types that support +/// lookup by String. +public protocol DynamicMemberLookup { + /// Get a value for a given `String` key + subscript(dynamicMember member: String) -> Any? { get } +} + +public extension DynamicMemberLookup where Self: RawRepresentable { + subscript(dynamicMember member: String) -> Any? { + switch member { + case "rawValue": return rawValue + default: return nil + } + } +} diff --git a/Sources/Variable.swift b/Sources/Variable.swift index 995706c..ca2d245 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -110,6 +110,8 @@ public struct Variable: Equatable, Resolvable { return object.value(forKey: bit) } #endif + } else if let value = context as? DynamicMemberLookup { + return value[dynamicMember: bit] } else if let value = context { return Mirror(reflecting: value).getValue(for: bit) } diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index e834064..52b276d 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -30,6 +30,17 @@ private class Blog: WebSite { let featuring: Article? = Article(author: Person(name: "Jhon")) } +@dynamicMemberLookup +private struct DynamicStruct: DynamicMemberLookup { + subscript(dynamicMember member: String) -> Any? { + member == "test" ? "this is a dynamic response" : nil + } +} + +private enum DynamicEnum: String, DynamicMemberLookup { + case someValue = "this is raw value" +} + final class VariableTests: XCTestCase { let context: Context = { let ext = Extension() @@ -49,7 +60,11 @@ final class VariableTests: XCTestCase { ], "article": Article(author: Person(name: "Kyle")), "blog": Blog(), - "tuple": (one: 1, two: 2) + "tuple": (one: 1, two: 2), + "dynamic": [ + "enum": DynamicEnum.someValue, + "struct": DynamicStruct() + ] ], environment: environment) #if os(OSX) context["object"] = Object() @@ -158,6 +173,20 @@ final class VariableTests: XCTestCase { } } + func testDynamicMemberLookup() { + it("can resolve dynamic member lookup") { + let variable = Variable("dynamic.struct.test") + let result = try variable.resolve(self.context) as? String + try expect(result) == "this is a dynamic response" + } + + it("can resolve dynamic enum rawValue") { + let variable = Variable("dynamic.enum.rawValue") + let result = try variable.resolve(self.context) as? String + try expect(result) == "this is raw value" + } + } + func testReflection() { it("can resolve a property with reflection") { let variable = Variable("article.author.name") diff --git a/docs/templates.rst b/docs/templates.rst index 7094ae6..4708328 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -22,6 +22,7 @@ following lookup: - Dictionary lookup - Array and string lookup (first, last, count, by index) - Key value coding lookup +- @dynamicMemberLookup when conforming to our `DynamicMemberLookup` marker protocol - Type introspection (via ``Mirror``) For example, if `people` was an array: