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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/.build
/.swiftlint-cache
/Packages
xcuserdata
project.xcworkspace
6 changes: 4 additions & 2 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ only_rules:
- number_separator
- opening_brace
- operator_usage_whitespace
- operator_whitespace
- function_name_whitespace
- overridden_super_call
- prefer_self_in_static_references
- prefer_self_type_over_type_of_self
Expand All @@ -104,7 +104,7 @@ only_rules:
- redundant_discardable_let
- redundant_nil_coalescing
- redundant_objc_attribute
- redundant_optional_initialization
- implicit_optional_initialization
- redundant_set_access_control
- redundant_string_enum_value
- redundant_type_annotation
Expand Down Expand Up @@ -153,6 +153,8 @@ only_rules:
- xct_specific_matcher
- xctfail_message
- yoda_condition
excluded:
- .build
analyzer_rules:
- capture_variable
- unused_declaration
Expand Down
17 changes: 8 additions & 9 deletions Sources/Defaults/Defaults+Bridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -323,16 +323,15 @@ extension Defaults {

if Bound.isNativelySupportedType {
return [value.lowerBound, value.upperBound]
} else {
guard
let lowerBound = Bound.bridge.serialize(value.lowerBound as? Bound.Value),
let upperBound = Bound.bridge.serialize(value.upperBound as? Bound.Value)
else {
return nil
}

return [lowerBound, upperBound]
}
guard
let lowerBound = Bound.bridge.serialize(value.lowerBound as? Bound.Value),
let upperBound = Bound.bridge.serialize(value.upperBound as? Bound.Value)
else {
return nil
}

return [lowerBound, upperBound]
}

public func deserialize(_ object: Serializable?) -> Value? {
Expand Down
5 changes: 2 additions & 3 deletions Sources/Defaults/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ extension Defaults.Serializable {
return Value.toValue(anyObject)
```
*/
static func toValue<T: Defaults.Serializable>(_ anyObject: Any, type: T.Type = Self.self) -> T? {
public static func toValue<T: Defaults.Serializable>(_ anyObject: Any, type: T.Type = Self.self) -> T? {
if
T.isNativelySupportedType,
let anyObject = anyObject as? T
Expand Down Expand Up @@ -228,8 +228,7 @@ extension Defaults.Serializable {
set(Value.toSerialize(value), forKey: key)
```
*/
@usableFromInline
static func toSerializable<T: Defaults.Serializable>(_ value: T) -> Any? {
public static func toSerializable<T: Defaults.Serializable>(_ value: T) -> Any? {
guard !T.isNativelySupportedType else {
return value
}
Expand Down
61 changes: 61 additions & 0 deletions Sources/DefaultsMacros/SerdeDefault.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Defaults

/**
Supported arguments for `@serde`.
*/
public enum SerdeOption {
case skip
}

/**
Attached macro that synthesizes `Defaults.Serializable` conformance for a struct.

This macro generates:

1. A nested `Bridge` type named `<TypeName>Bridge`.
2. `serialize(_:)` and `deserialize(_:)` implementations based on stored properties.
3. A static `bridge` instance used by `Defaults`.

For example, given:

```swift
@SerdeDefault
struct User {
var name: String
var age: UInt?
}
```

The macro generates an extension equivalent to:

```swift
extension User: Defaults.Serializable {
struct UserBridge: Defaults.Bridge {
typealias Value = User
typealias Serializable = [String: Any]
// serialize(_:) and deserialize(_:)
}

static let bridge = UserBridge()
}
```

- Important: In this stage, `@SerdeDefault` supports `struct` declarations only.
*/
@attached(extension, conformances: Defaults.Serializable, names: named(bridge), suffixed(Bridge))
public macro SerdeDefault() = #externalMacro(module: "DefaultsMacrosDeclarations", type: "SerdeDefaultMacro")

/**
Peer macro used on stored properties in `@SerdeDefault` structs to configure serde behavior.

Supported arguments:

1. `@serde(.skip)`: Excludes the property from serialization and deserialization.

- Important: `skip` is only valid on optional properties.
*/
@attached(peer)
public macro serde(_ options: SerdeOption...) = #externalMacro(
module: "DefaultsMacrosDeclarations",
type: "SerdeMacro"
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import SwiftSyntaxMacros
@main
struct DefaultsMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
ObservableDefaultMacro.self
ObservableDefaultMacro.self,
SerdeDefaultMacro.self,
SerdeMacro.self
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import SwiftSyntax

extension SerdeDefault {
/**
Code generator for func deserialize code blocks.

For native properties:

```swift
let property = base["property"] as? PropertyType
```
For serializable properties:

```swift
let property = PropertyType.toValue(base["property"] as Any, type: PropertyType.self)
```
*/
enum Deserialize: CodeGenerating {
struct Context {
/**
The serialized dictionary parameter identifier being deserialized.
*/
let base: TokenSyntax
/**
The nominal type being rebuilt.
*/
let typeSyntax: TypeSyntax
}

/**
Generated deserialization code for one property.

- `omitted`: Property not in the memberwise initializer — excluded entirely.
- `skipped`: Property marked with `@serde(.skip)` — passed as `nil` in the initializer.
- `assignment`: Normal property with a binding statement and an initializer argument.
*/
enum CodeGenerated {
case omitted
case skipped(identifier: TokenSyntax)
case assignment(identifier: TokenSyntax, statement: CodeBlockItemSyntax)
}

struct DeserializedCodeGenerated {
let codesGenerated: [CodeGenerated]
let typeSyntax: TypeSyntax

/**
The list of statements for initializing the properties.
*/
var statements: CodeBlockItemListSyntax {
let statements = codesGenerated.compactMap { code -> CodeBlockItemSyntax? in
guard case .assignment(_, let statement) = code else {
return nil
}
return statement
}
return CodeBlockItemListSyntax(
statements.enumerated().map { index, statement in
statement.with(\.leadingTrivia, index == 0 ? [] : .newline)
}
)
}

/**
The initializer expression for the nominal type, using the generated arguments.


*/
var initializer: FunctionCallExprSyntax {
let callee: ExprSyntax = "\(typeSyntax.trimmed)"
let arguments = codesGenerated.compactMap { code -> (label: TokenSyntax, expression: ExprSyntax)? in
switch code {
case .omitted:
return nil
// Properties marked with `@serde(.skip)` are passed as `nil` in the initializer.
case .skipped(let identifier):
return (identifier, ExprSyntax(NilLiteralExprSyntax()))
case .assignment(let identifier, _):
return (identifier, ExprSyntax(identifier.declReference()))
}
}
let labeled = arguments.enumerated().map { index, argument in
LabeledExprSyntax(
label: argument.label,
colon: .colonToken(),
expression: argument.expression,
trailingComma: index < arguments.count - 1 ? .commaToken() : nil
)
}
return FunctionCallExprSyntax(callee: callee) {
LabeledExprListSyntax(labeled)
}
}
}

static func codeGeneratedBlock(
from properties: [any Property],
in context: Context
) -> CodeBlockSyntax {
let codesGenerated = properties.map { property in
property.deserialize(in: context)
}
let codeGenerated = DeserializedCodeGenerated(
codesGenerated: codesGenerated,
typeSyntax: context.typeSyntax
)

return CodeBlockSyntax(
"""
{
guard let \(context.base) else { return nil }
\(codeGenerated.statements)
return \(codeGenerated.initializer)
}
"""
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import SwiftSyntax
import SwiftSyntaxMacros

extension SerdeDefault {
/**
Code generator for func serialize code blocks.

For native properties:

```swift
serialized["property"] = value.property
```

For serializable properties:

```swift
guard let property = PropertyType.toSerializable(value.property) else { return nil }
serialized["property"] = property
```
*/
enum Serialize: CodeGenerating {
struct Context {
/**
The value parameter identifier being serialized.
*/
let base: TokenSyntax
/**
The dictionary identifier that receives serialized values.
*/
let serialized: TokenSyntax
/**
The expansion context, used to mint collision-free identifiers.
*/
let macroContext: any MacroExpansionContext

/**
Returns a collision-free name for the temporary that holds `identifier`'s value.

`base` and `serialized` are the only other identifiers declared in the generated
function, and property names reach it only as member accesses (`value.name`) or
dictionary keys. Minting the temporaries is therefore enough to keep the whole
function hygienic — a property named `serialized` would otherwise shadow the
dictionary it is being written into.
*/
func makeUniqueName(for identifier: TokenSyntax) -> TokenSyntax {
macroContext.makeUniqueName(identifier.text)
}
}

/**
Generated serialization code for one property.

- `omitted`: Property excluded from serialization (e.g. marked with `@serde(.skip)`).
- `basic`: Direct dictionary entries for non-optional native values.
- `optional`: Conditional assignments for optional values.
- `serializable`: Conversion code for values backed by `Defaults.Serializable`.
*/
enum CodeGenerated {
case omitted
case basic(DictionaryElementSyntax)
case optional(CodeBlockItemSyntax)
case serializable(CodeBlockItemSyntax)
}

struct SerializedCodeGenerated {
let codesGenerated: [CodeGenerated]

var initializer: ExprSyntax {
let initializerElements = codesGenerated.compactMap { code in
if case .basic(let element) = code {
return element
}
return nil
}
if initializerElements.isEmpty {
return ExprSyntax("[:]")
}
return ExprSyntax(
stringLiteral: "[\(initializerElements.map(\.description).joined(separator: ", "))]"
)
}

var assignments: CodeBlockItemListSyntax {
let assignments: [CodeBlockItemSyntax] = codesGenerated.compactMap { code in
switch code {
case .omitted, .basic:
return nil
case .optional(let code), .serializable(let code):
return code
}
}
return CodeBlockItemListSyntax(assignments)
}
}

static func codeGeneratedBlock(
from properties: [any Property],
in context: Context
) -> CodeBlockSyntax {
let codesGenerated = properties.map { property in
property.serialize(in: context)
}
let codeGenerated = SerializedCodeGenerated(codesGenerated: codesGenerated)
// Only bind with `var` when something actually mutates the dictionary, otherwise
// every expansion warns that it was never mutated.
let bindingSpecifier: TokenSyntax =
codeGenerated.assignments.isEmpty ? .keyword(.let) : .keyword(.var)

return CodeBlockSyntax(
"""
{
guard let \(context.base) else { return nil }
\(bindingSpecifier) \(context.serialized): \(IdentifierTypeSyntax.Serializable) = \(codeGenerated.initializer)
\(codeGenerated.assignments)
return \(context.serialized)
}
"""
)
}
}
}
Loading