Skip to content

Commit

Permalink
refactor: modify macro implementation (#12)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: replaced `CodablePath` with `CodedAt`
BREAKING CHANGE: replaced `CodableCompose` with `CodedAt` without args
BREAKING CHANGE: renamed `ExternalHelperCoder` to `HelperCoder`
BREAKING CHANGE: replaced `default:` with `@Default`
BREAKING CHANGE: replaced `helper:` with `@CodedBy`
  • Loading branch information
soumyamahunt committed Sep 20, 2023
1 parent a6bc3a2 commit 8d61676
Show file tree
Hide file tree
Showing 55 changed files with 4,954 additions and 1,414 deletions.
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Swift",
"image": "swift:nightly-5.9-jammy",
"image": "swiftlang/swift:nightly-5.9-jammy",
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"installZsh": "false",
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ let package = Package(
.library(name: "MetaCodable", targets: ["MetaCodable"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-17-a"),
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-07-09-a"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"),
],
targets: [
Expand Down
57 changes: 42 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@ Supercharge `Swift`'s `Codable` implementations with macros.

`MetaCodable` framework exposes custom macros which can be used to generate dynamic `Codable` implementations. The core of the framework is ``Codable()`` macro which generates the implementation aided by data provided with using other macros.


`MetaCodable` aims to supercharge your `Codable` implementations by providing these inbox features:

- Allows custom `CodingKey` value declaration per variable, instead of requiring you to write all the `CodingKey` values with ``CodablePath(_:)`` etc.
- Allows to create flattened model for nested `CodingKey` values with ``CodablePath(_:)`` etc.
- Allows to create composition of multiple `Codable` types with ``CodableCompose()`` etc.
- Allows to provide default value in case of decoding failures with ``CodablePath(default:_:)`` and ``CodableCompose(default:)`` etc.
- Allows custom `CodingKey` value declaration per variable, instead of requiring you to write all the `CodingKey` values with ``CodedAt(_:)`` passing single argument.
- Allows to create flattened model for nested `CodingKey` values with ``CodedAt(_:)`` and ``CodedIn(_:)``.
- Allows to create composition of multiple `Codable` types with ``CodedAt(_:)`` passing no arguments.
- Allows to provide default value in case of decoding failures with ``Default(_:)``.
- Generates member-wise initializer considering the above default value syntax as well.
- Allows to create custom decoding/encoding strategies with ``ExternalHelperCoder``. i.e. ``LossySequenceCoder`` etc.
- Allows to create custom decoding/encoding strategies with ``HelperCoder`` and using them with ``CodedBy(_:)``. i.e. ``LossySequenceCoder`` etc.

## Requirements

Expand Down Expand Up @@ -51,6 +50,7 @@ Then you can add the `MetaCodable` module product as dependency to the `target`s
```swift
.product(name: "MetaCodable", package: "MetaCodable"),
```

</details>

## Usage
Expand All @@ -61,13 +61,14 @@ Then you can add the `MetaCodable` module product as dependency to the `target`s
<summary>Custom `CodingKey` value declaration per variable, instead of requiring you to write for all fields.</summary>

i.e. in the official [docs](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types#2904057), to define custom `CodingKey` for 2 fields of `Landmark` type you had to write:

```swift
struct Landmark: Codable {
var name: String
var foundingYear: Int
var location: Coordinate
var vantagePoints: [Coordinate]

enum CodingKeys: String, CodingKey {
case name = "title"
case foundingYear = "founding_date"
Expand All @@ -76,25 +77,29 @@ struct Landmark: Codable {
}
}
```

But with `MetaCodable` all you have to write is this:

```swift
@Codable
struct Landmark {
@CodablePath("title")
@CodedAt("title")
var name: String
@CodablePath("founding_date")
@CodedAt("founding_date")
var foundingYear: Int

var location: Coordinate
var vantagePoints: [Coordinate]
}
```

</details>

<details>
<summary>Create flattened model for nested `CodingKey` values.</summary>

i.e. in official [docs](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types#2904058) to decode a JSON like this:

```json
{
"latitude": 0,
Expand All @@ -104,7 +109,9 @@ i.e. in official [docs](https://developer.apple.com/documentation/foundation/arc
}
}
```
You had to write all these boilerplate:

You had to write all these boilerplate:

```swift
struct Coordinate {
var latitude: Double
Expand All @@ -127,7 +134,7 @@ extension Coordinate: Decodable {
let values = try decoder.container(keyedBy: CodingKeys.self)
latitude = try values.decode(Double.self, forKey: .latitude)
longitude = try values.decode(Double.self, forKey: .longitude)

let additionalInfo = try values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
elevation = try additionalInfo.decode(Double.self, forKey: .elevation)
}
Expand All @@ -138,42 +145,62 @@ extension Coordinate: Encodable {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(latitude, forKey: .latitude)
try container.encode(longitude, forKey: .longitude)

var additionalInfo = container.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
try additionalInfo.encode(elevation, forKey: .elevation)
}
}
```

But with `MetaCodable` all you have to write is this:

```swift
@Codable
struct Coordinate {
var latitude: Double
var longitude: Double
@CodablePath("additionalInfo", "elevation")

@CodedAt("additionalInfo", "elevation")
var elevation: Double
}
```

You can even minimize further using `CodedIn` macro since the final `CodingKey` value is the same as field name:

```swift
@Codable
struct Coordinate {
var latitude: Double
var longitude: Double

@CodedIn("additionalInfo")
var elevation: Double
}
```

</details>

<details>
<summary>Provide default value in case of decoding failures and member-wise initializer generated considers these default values.</summary>

Instead of throwing error in case of missing data or type mismatch, you can provide a default value that will be assigned in this case. The memberwise initializer generated also uses this default value for the field. The following definition with `MetaCodable`:

```swift
@Codable
struct CodableData {
@CodablePath(default: "some")
@Default("some")
let field: String
}
```

will not throw any error when empty JSON(`{}`) or JSON with type mismatch(`{ "field": 5 }`) is provided. The default value will be assigned in such case. Also, the memberwise initializer generated will look like this:

```swift
init(field: String = "some") {
self.field = field
}
```

</details>

See the full [documentation](https://swiftylab.github.io/MetaCodable/documentation/metacodable/) for API details and advanced use cases.
Expand Down
72 changes: 72 additions & 0 deletions Sources/CodableMacroPlugin/Attributes/AttributableDeclSyntax.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import SwiftSyntax

/// An extension that manages fetching attributes
/// attached to declarations.
extension SyntaxProtocol {
/// Provides all the attributes attached to this declaration of
/// the provided type.
///
/// All the attribute syntaxes are checked and those matching
/// the provided type are returned.
///
/// - Parameter type: The macro-attribute type to search.
/// - Returns: All the attributes of provided type.
func attributes<A: Attribute>(for type: A.Type) -> [A] {
guard
case .choices(let choices) = Self.structure
else { return [] }

let declSyntaxChoice = choices.first { choice in
if case .node(let type) = choice {
return type is AttributableDeclSyntax.Type
&& self.is(type)
} else {
return false
}
}

guard
let declSyntaxChoice,
case .node(let declSyntaxType) = declSyntaxChoice,
let declaration = self.as(declSyntaxType),
let declaration = declaration as? AttributableDeclSyntax
else { return [] }

return declaration.attributes?.compactMap { attribute in
guard case .attribute(let attribute) = attribute else { return nil }
return type.init(from: attribute)
} ?? []
}
}

/// A declaration syntax type that supports macro-attribute.
///
/// This type can check whether an `AttributeSyntax`
/// is for this attribute and perform validation of this attribute usage.
protocol AttributableDeclSyntax: DeclSyntaxProtocol {
/// The list of attributes attached to this declaration.
var attributes: AttributeListSyntax? { get }
}

extension AccessorDeclSyntax: AttributableDeclSyntax {}
extension ActorDeclSyntax: AttributableDeclSyntax {}
extension AssociatedtypeDeclSyntax: AttributableDeclSyntax {}
extension ClassDeclSyntax: AttributableDeclSyntax {}
extension DeinitializerDeclSyntax: AttributableDeclSyntax {}
extension EditorPlaceholderDeclSyntax: AttributableDeclSyntax {}
extension EnumCaseDeclSyntax: AttributableDeclSyntax {}
extension EnumDeclSyntax: AttributableDeclSyntax {}
extension ExtensionDeclSyntax: AttributableDeclSyntax {}
extension FunctionDeclSyntax: AttributableDeclSyntax {}
extension ImportDeclSyntax: AttributableDeclSyntax {}
extension InitializerDeclSyntax: AttributableDeclSyntax {}
extension MacroDeclSyntax: AttributableDeclSyntax {}
extension MacroExpansionDeclSyntax: AttributableDeclSyntax {}
extension MissingDeclSyntax: AttributableDeclSyntax {}
extension OperatorDeclSyntax: AttributableDeclSyntax {}
extension PrecedenceGroupDeclSyntax: AttributableDeclSyntax {}
extension ProtocolDeclSyntax: AttributableDeclSyntax {}
extension StructDeclSyntax: AttributableDeclSyntax {}
extension SubscriptDeclSyntax: AttributableDeclSyntax {}
extension TypealiasDeclSyntax: AttributableDeclSyntax {}
extension VariableDeclSyntax: AttributableDeclSyntax {}
72 changes: 72 additions & 0 deletions Sources/CodableMacroPlugin/Attributes/Attribute.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import SwiftSyntax
import SwiftDiagnostics
import SwiftSyntaxMacros

/// A type indicating a macro-attribute.
///
/// This type can check whether an `AttributeSyntax`
/// is for this attribute and perform validation and code generation
/// for this attribute usage.
protocol Attribute: AttachedMacro {
/// The name of this attribute.
static var name: String { get }
/// The syntax used for this attribute instance.
var node: AttributeSyntax { get }
/// Creates a new instance with the provided node
///
/// The initializer fails to create new instance if the name
/// of the provided node is different than this attribute.
///
/// - Parameter node: The attribute syntax to create with.
/// - Returns: Newly created attribute instance.
init?(from node: AttributeSyntax)
/// Validates this attribute is used properly with the declaration provided.
///
/// This type checks the attribute usage doesn't violate any conditions
/// and produces diagnostics for such violations in the macro expansion
/// context provided.
///
/// - Parameters:
/// - declaration: The declaration this macro attribute is attached to.
/// - context: The macro expansion context validation performed in.
///
/// - Returns: True if attribute usage satisfies all conditions,
/// false otherwise.
@discardableResult
func validate(
declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) -> Bool
}

extension Attribute {
/// The name of this attribute.
///
/// Type name is used as attribute name.
static var name: String { "\(Self.self)" }
/// The name of this attribute.
///
/// By default type name is used as attribute name.
var name: String { Self.name }
/// The lowercased-name of this attribute.
///
/// This is used for attribute related diagnostics.
var id: String { name.lowercased() }

/// Message id for misuse of this attribute.
///
/// This attribute can must be removed or its usage condition must be satisfied.
var misuseMessageID: MessageID { .messageID("\(id)-misuse") }
/// Message id for unnecessary usage of this attribute.
///
/// This attribute can be omitted in such scenario and the final result will still be the same.
var unusedMessageID: MessageID { .messageID("\(id)-unused") }

/// Checks whether this attribute is applied more than once to provided declaration.
///
/// - Parameter declaration: The declaration this macro attribute is attached to.
/// - Returns: Whether this attribute is applied more than once.
func isDuplicated(in declaration: some SyntaxProtocol) -> Bool {
return declaration.attributes(for: Self.self).count > 1
}
}

0 comments on commit 8d61676

Please sign in to comment.