Skip to content

Instantly share code, notes, and snippets.

@amirdew
Last active May 29, 2024 06:05
Show Gist options
  • Save amirdew/ff8e27af5aee5f2bb3c2da2c9f7327cd to your computer and use it in GitHub Desktop.
Save amirdew/ff8e27af5aee5f2bb3c2da2c9f7327cd to your computer and use it in GitHub Desktop.
Modifying private and immutable properties (let) in Codable instances
import Foundation
extension Decodable where Self: Encodable {
/// Creates a new instance and changes the value for the provided key.
///
/// - Parameters:
/// - key: The key path to the property that you want to modify.
/// Use period to separate levels and [] for indexes.
/// Examples: "id", "name.firstName", "children[2].name.firstName"
///
/// - value: The new value for the provided key.
///
/// - Returns: A new instance with modified property
func modifying<T>(_ key: String, to value: T) throws -> Self {
let originalData = try JSONEncoder().encode(self)
var object = try JSONSerialization.jsonObject(with: originalData, options: [])
let keyComponents = keyComponents(from: key)
object = modify(object, keyComponents: keyComponents, value: value)
let data = try JSONSerialization.data(withJSONObject: object, options: [])
return try JSONDecoder().decode(Self.self, from: data)
}
private func modify<T>(_ object: Any, keyComponents: [KeyComponent], value: T) -> Any {
var keyComponents = keyComponents
while !keyComponents.isEmpty {
let keyComponent = keyComponents.removeFirst()
if var array = object as? [Any], case .index(let index) = keyComponent, index < array.count {
if keyComponents.isEmpty {
array[index] = value
} else {
var nextObject = array[index]
nextObject = modify(nextObject, keyComponents: keyComponents, value: value)
array[index] = nextObject
}
return array
} else if var dictionary = object as? [String: Any], case .key(let key) = keyComponent {
if keyComponents.isEmpty {
dictionary[key] = value
} else if var nextObject = dictionary[key] {
nextObject = modify(nextObject, keyComponents: keyComponents, value: value)
dictionary[key] = nextObject
}
return dictionary
}
}
return object
}
private func keyComponents(from key: String) -> [KeyComponent] {
let indexSearch = /(.+)\[(\d+)\]/
return key.components(separatedBy: ".").flatMap { keyComponent in
if let result = try? indexSearch.wholeMatch(in: keyComponent), let index = Int(result.2) {
return [KeyComponent.key(String(result.1)), .index(index)]
}
return [.key(keyComponent)]
}
}
}
private enum KeyComponent {
case key(String)
case index(Int)
}
@amirdew
Copy link
Author

amirdew commented Dec 8, 2022

Usage:

// Simple

struct File: Codable {
    let name: String
    let size: Int
}

let file = File(name: "image.png", size: 256)

let modifiedFile = try file
    .modifying("name", to: "video.mp4")
    .modifying("size", to: 512)

dump(modifiedFile)

/*
 File
  - name: "video.mp4"
  - size: 512
 */

// Complex

struct Name: Codable {
    let firstName: String
    let lastName: String
}

struct User: Codable {
    let id: Int
    let name: Name
    let children: [User]
}

let userA = User(
    id: 1,
    name: Name(firstName: "A1", lastName: "A1L"),
    children: [
        User(id: 11, name: Name(firstName: "A11", lastName: "A11L"), children: [
            User(id: 111, name: Name(firstName: "A111", lastName: "A111L"), children: [])
        ]),
        User(id: 12, name: Name(firstName: "A12", lastName: "A12L"), children: [])
    ]
)

let userB = try userA
    .modifying("name.firstName", to: "B1")
    .modifying("id", to: 2)
    .modifying("children[1].name.lastName", to: "B12L")
    .modifying("children[0].children[0].name.firstName", to: "B111")

dump(userB)
/*
 User
   - id: 2
   ▿ name: Name
     - firstName: "B1"
     - lastName: "A1L"
   ▿ children: 2 elements
     ▿ User
       - id: 11
       ▿ name: Name
         - firstName: "A11"
         - lastName: "A11L"
       ▿ children: 1 element
         ▿ User
           - id: 111
           ▿ name: Name
             - firstName: "B111"
             - lastName: "A111L"
           - children: 0 elements
     ▿ User
       - id: 12
       ▿ name: Name
         - firstName: "A12"
         - lastName: "B12L"
       - children: 0 elements
 */

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment