Skip to content

Commit

Permalink
feat: added actor support
Browse files Browse the repository at this point in the history
  • Loading branch information
soumyamahunt committed Jan 9, 2024
1 parent a22e9d1 commit 97a6057
Show file tree
Hide file tree
Showing 13 changed files with 270 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,16 @@ extension Codable: MemberMacro, ExtensionMacro {
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard
let exp = AttributeExpander(for: declaration, in: context),
let decl = declaration.as(ClassDeclSyntax.self),
case let type = IdentifierTypeSyntax(name: decl.name)
let exp = AttributeExpander(for: declaration, in: context)
else { return [] }
let type: IdentifierTypeSyntax
if let decl = declaration.as(ClassDeclSyntax.self) {
type = .init(name: decl.name)
} else if let decl = declaration.as(ActorDeclSyntax.self) {
type = .init(name: decl.name)
} else {
return []
}
let exts = exp.codableExpansion(for: type, to: protocols, in: context)
return exts.flatMap { `extension` in
`extension`.memberBlock.members.map { DeclSyntax($0.decl) }
Expand Down Expand Up @@ -120,7 +126,9 @@ extension Codable: MemberMacro, ExtensionMacro {
let exp = AttributeExpander(for: declaration, in: context)
else { return [] }
var exts = exp.codableExpansion(for: type, to: protocols, in: context)
if declaration.is(ClassDeclSyntax.self) {
if declaration.is(ClassDeclSyntax.self)
|| declaration.is(ActorDeclSyntax.self)
{
for (index, var `extension`) in exts.enumerated() {
`extension`.memberBlock = .init(members: [])
exts[index] = `extension`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ struct Codable: Attribute {
return AggregatedDiagnosticProducer {
expect(
syntaxes: StructDeclSyntax.self, ClassDeclSyntax.self,
EnumDeclSyntax.self
EnumDeclSyntax.self, ActorDeclSyntax.self
)
cantDuplicate()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ struct MemberInit: Attribute {
/// - Returns: The built diagnoser instance.
func diagnoser() -> DiagnosticProducer {
return AggregatedDiagnosticProducer {
expect(syntaxes: StructDeclSyntax.self)
expect(syntaxes: StructDeclSyntax.self, ActorDeclSyntax.self)
cantDuplicate()
}
}
Expand Down
18 changes: 18 additions & 0 deletions Sources/CodableMacroPlugin/Variables/ComposedVariable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,21 @@ where Self: EnumCaseVariable, Wrapped: EnumCaseVariable {
/// Provides associated variables of the underlying variable value.
var variables: [any AssociatedVariable] { base.variables }
}

extension ComposedVariable where Self: TypeVariable, Wrapped: TypeVariable {
/// Provides the syntax for `CodingKeys` declarations.
///
/// Provides members generated by the underlying variable value.
///
/// - Parameters:
/// - protocols: The protocols for which conformance generated.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: The `CodingKeys` declarations.
func codingKeys(
confirmingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) -> MemberBlockItemListSyntax {
return base.codingKeys(confirmingTo: protocols, in: context)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ extension ClassDeclSyntax: MemberGroupSyntax, VariableSyntax {
typealias Variable = ClassVariable
}

extension ActorDeclSyntax: MemberGroupSyntax, VariableSyntax {
/// The `Variable` type this syntax represents.
///
/// The actor variable type used with current declaration.
typealias Variable = ActorVariable
}

extension EnumDeclSyntax: MemberGroupSyntax, VariableSyntax {
/// The `Variable` type this syntax represents.
///
Expand Down
53 changes: 53 additions & 0 deletions Sources/CodableMacroPlugin/Variables/Type/ActorVariable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
@_implementationOnly import SwiftSyntax
@_implementationOnly import SwiftSyntaxMacros

/// A `TypeVariable` that provides `Codable` conformance
/// for an `actor` type.
///
/// This type can be used for `actor`s for `Decodable` conformance and
/// `Encodable` implementation without conformance.
struct ActorVariable: TypeVariable, DeclaredVariable, ComposedVariable,
InitializableVariable
{
/// The initialization type of this variable.
///
/// Initialization type is the same as underlying member group variable.
typealias Initialization = MemberGroup<ActorDeclSyntax>.Initialization
/// The member group used to generate conformance implementations.
let base: MemberGroup<ActorDeclSyntax>

/// Creates a new variable from declaration and expansion context.
///
/// Uses the actor declaration with member group to generate conformances.
///
/// - Parameters:
/// - decl: The declaration to read from.
/// - context: The context in which the macro expansion performed.
init(from decl: ActorDeclSyntax, in context: some MacroExpansionContext) {
self.base = .init(from: decl, in: context)
}

/// Provides the syntax for encoding at the provided location.
///
/// Uses member group to generate syntax, the implementation is added
/// while not conforming to `Encodable` protocol.
///
/// - Parameters:
/// - context: The context in which to perform the macro expansion.
/// - location: The encoding location.
///
/// - Returns: The generated encoding syntax.
func encoding(
in context: some MacroExpansionContext,
to location: TypeCodingLocation
) -> TypeGenerated? {
guard
let generated = base.encoding(in: context, to: location)
else { return nil }
return .init(
code: generated.code, modifiers: generated.modifiers,
whereClause: generated.whereClause,
inheritanceClause: nil
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,4 @@ protocol GenericTypeDeclSyntax {
extension StructDeclSyntax: GenericTypeDeclSyntax {}
extension ClassDeclSyntax: GenericTypeDeclSyntax {}
extension EnumDeclSyntax: GenericTypeDeclSyntax {}
extension ActorDeclSyntax: GenericTypeDeclSyntax {}
8 changes: 4 additions & 4 deletions Sources/MetaCodable/Codable/Codable.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// Generate `Codable` implementation of `struct`, `class`, `enum` types
/// by leveraging custom attributes provided on variable declarations.
/// Generate `Codable` implementation of `struct`, `class`, `enum`, `actor`
/// types by leveraging custom attributes provided on variable declarations.
///
/// # Usage
/// By default the field name is used as `CodingKey` for the field value during
Expand Down Expand Up @@ -38,8 +38,8 @@
/// * If attached declaration already conforms to `Codable` this macro expansion
/// is skipped.
///
/// - Important: The attached declaration must be of a `struct`, `class`
/// or `enum` type. [See the limitations for this macro](<doc:Limitations>).
/// - Important: The attached declaration must be of a `struct`, `class`, `enum`
/// or `actor` type. [See the limitations for this macro](<doc:Limitations>).
@attached(
extension, conformances: Decodable, Encodable,
names: named(CodingKeys), named(DecodingKeys),
Expand Down
24 changes: 23 additions & 1 deletion Sources/MetaCodable/CodedAt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,35 @@
/// }
/// ```
///
/// * For enums, this attribute can be used along with ``TaggedAt(_:_:)``
/// to support adjacently tagged enums. The path provided represents the path
/// where associated values of each case is decoded/encoded.
/// i.e. for JSON with following format:
/// ```json
/// {"t": "para", "c": [{...}, {...}]}
/// ```
/// ```json
/// {"t": "str", "c": "the string"}
/// ```
/// enum representation can be created:
/// ```swift
/// @Codable
/// @TaggedAt("t")
/// @CodedAt("c")
/// enum Block {
/// case para([Inline]),
/// case str(String),
/// }
/// ```
///
/// - Parameter path: The `CodingKey` path value located at.
///
/// - Note: This macro on its own only validates if attached declaration
/// is a variable declaration. ``Codable()`` macro uses this macro
/// when generating final implementations.
///
/// - Important: The field type must confirm to `Codable`.
/// - Important: When applied to fields, the field type must confirm to
/// `Codable`.
@attached(peer)
@available(swift 5.9)
public macro CodedAt(_ path: StaticString...) =
Expand Down
37 changes: 37 additions & 0 deletions Sources/MetaCodable/MetaCodable.docc/Limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,40 @@ struct Model {
The ability to pass conformance data to macro for classes when performing member attribute expansion was introduced in [`Swift 5.9.2`](https://github.com/apple/swift-evolution/blob/main/proposals/0407-member-macro-conformances.md). Please make sure to upgrade to this version to have this working.

Even with this it is unable for ``Codable()`` to get clear indication where conformance to `Codable` is implemented by current class or the super class. ``Codable()`` checks current class for the conformance implementation by checking implementation functions and the check will not work if some `typealias` used for `Decoder`/`Encoder` in implementation function definition.

### Why enum-case associated values decoding/encoding are not customizable?

The goal of ``MetaCodable`` is to allow same level of customization for enum-case associated values as it is allowed for `struct`/`class`/`actor` member properties. Unfortunately, as of now, `Swift` doesn't allow macro attributes (or any attributes) to be attached per enum-case arguments.

[A pitch has been created to allow this support in `Swift`](https://forums.swift.org/t/attached-macro-support-for-enum-case-arguments/67952), you can support this pitch on `Swift` forum if this feature will benefit you.

The current workaround is to extract enum-case arguments to separate `struct` and have the customization options in the `struct` itself. i.e. since following isn't possible:

```swift
@Codable
enum SomeEnum {
case string(@CodedAt("data") String)
}
```

you can convert it to:

```swift
@Codable
enum SomeEnum {
case string(StringData)

@Codable
struct StringData {
let data: String
}
}
```

### Why `actor` conformance to `Encodable` not generated?

For `actor`s ``Codable()`` generates `Decodable` conformance, while `Encodable` conformance isn't generated, only `encode(to:)` method implementation is generated which is isolated to `actor`.

To generate `Encodable` conformance, the `encode(to:)` method must be `nonisolated` to `actor`, and since `encode(to:)` method must be synchronous making it `nonisolated` will prevent accessing mutable properties.

Due to these limitations, `Encodable` conformance isn't generated, users has to implement the conformance manually.
28 changes: 0 additions & 28 deletions Tests/MetaCodableTests/CodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,6 @@ import XCTest

final class CodableTests: XCTestCase {

func testMisuseOnInvalidDeclaration() throws {
assertMacroExpansion(
"""
@Codable
actor SomeCodable {
let value: String
}
""",
expandedSource:
"""
actor SomeCodable {
let value: String
}
""",
diagnostics: [
.init(
id: Codable.misuseID,
message:
"@Codable only applicable to struct or class or enum declarations",
line: 1, column: 1,
fixIts: [
.init(message: "Remove @Codable attribute")
]
)
]
)
}

func testWithoutAnyCustomization() throws {
assertMacroExpansion(
"""
Expand Down
53 changes: 53 additions & 0 deletions Tests/MetaCodableTests/CodedAt/CodedAtTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -650,5 +650,58 @@ final class CodedAtTests: XCTestCase {
"""
)
}

func testActorWithNestedPathOnMixedTypes() throws {
assertMacroExpansion(
"""
@Codable
@MemberInit
actor SomeCodable {
@CodedAt("deeply", "nested", "key1")
let value1: String
@CodedAt("deeply", "nested", "key2")
var value2: String?
}
""",
expandedSource:
"""
actor SomeCodable {
let value1: String
var value2: String?
init(value1: String, value2: String? = nil) {
self.value1 = value1
self.value2 = value2
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let deeply_container = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.deeply)
let nested_deeply_container = try deeply_container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.nested)
self.value1 = try nested_deeply_container.decode(String.self, forKey: CodingKeys.value1)
self.value2 = try nested_deeply_container.decodeIfPresent(String.self, forKey: CodingKeys.value2)
}
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var deeply_container = container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.deeply)
var nested_deeply_container = deeply_container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.nested)
try nested_deeply_container.encode(self.value1, forKey: CodingKeys.value1)
try nested_deeply_container.encodeIfPresent(self.value2, forKey: CodingKeys.value2)
}
enum CodingKeys: String, CodingKey {
case value1 = "key1"
case deeply = "deeply"
case nested = "nested"
case value2 = "key2"
}
}
extension SomeCodable: Decodable {
}
"""
)
}
}
#endif

0 comments on commit 97a6057

Please sign in to comment.