1

I am trying to mock Apollo Queries using its init. It pretty much is taking in a dictionary to build the object up.

public init(unsafeResultMap: [String: Any]) {
  self.resultMap = unsafeResultMap
}

So, I have decided to create Mock objects that have the same properties of the query objects while being Encodable (So we get the free JSON conversion, which can be represented as a string version dictionary).

For example:

class MockAnimalObject: Encodable {
  let teeth: MockTeethObject

  init(teeth: MockTeethObject) {
    self.teeth = teeth
  }
}

class MockTeethObject: Encodable {
  let numberOfTeeth: Int
  let dateOfTeethCreation: Date

  init (numberOfTeeth: Int, dateOfTeethCreation: Date) {
    self.numberOfTeeth = numberOfTeeth
    self.dateOfTeethCreation = dateOfTeethCreation
  }
}

The problem is, the Apollo conversion checks the types during the result map, which in our case is a string of [String: Encodable].

And this is where the Date encodable becomes a problem.

/// This property is auto-generated and not feasible to be edited

/// Teeth date for these teeth
public var teethCreationDate: Date { 
  get {
    // This is the problem. resultMap["teethCreationDate"] is never going to be a Date object since it is encoded. 
    return resultMap["teethCreationDate"]! as! Date 
  }
  set {
    resultMap.updateValue(newValue, forKey: "teethCreationDate")
  }
}

So, I am wondering if it is possible to override the encoder to manually set the date value as a custom type.

var container = encoder.singleValueContainer()
try container.encode(date) as Date // Something where I force it to be a non-encodable object
Mocha
  • 2,035
  • 12
  • 29
  • 3
    Json is a string message so I don’t understand what you mean when you say your property should be encoded into a Date. – Joakim Danielson Aug 12 '22 at 06:02
  • I used JSON for the easy object to dictionary conversion. However, I want to modify the JSON to allow a [String: Date] – Mocha Aug 12 '22 at 16:33
  • 1
    Your question makes no sense. JSON doesn't deal with anything other than strings, numbers and booleans (plus arrays or dictionaries that also consist of strings, numbers and booleans). – Rudedog Aug 12 '22 at 16:55
  • 1
    Json is not a dictionary, if your exptected output is a dictionary then forget about json – Joakim Danielson Aug 12 '22 at 17:11

2 Answers2

3

JSON has nothing to do with this. JSON is not any kind of dictionary. It's a serialization format. But you don't want a serialization format. You want to convert types to an Apollo ResultMap, which is [String: Any?]. What you want is a "ResultMapEncoder," not a JSONEncoder.

That's definitely possible. It's just an obnoxious amount of code because Encoder is such a pain to conform to. My first pass is a bit over 600 lines. I could probably strip it down more and it's barely tested, so I don't know if this code works in all (or even most) cases, but it's a start and shows how you would attack this problem.

The starting point is the source code for JSONEncoder. Like sculpture, you start with a giant block of material, and keep removing everything that doesn't look like what you want. Again, this is very, very lightly tested. It basically does what you describe in your question, and not much else is tested.

let animal = MockAnimalObject(teeth: MockTeethObject(numberOfTeeth: 10, 
                              dateOfTeethCreation: .now))

let result = try AnyEncoder().encode(animal)
print(result)

//["teeth": Optional(["dateOfTeethCreation": Optional(2022-08-12 18:35:27 +0000),
// "numberOfTeeth": Optional(10)])]

The key changes, and where you'd want to explore further to make this work the way you want, are:

  • Gets rid of all configuration and auto-conversions (like snake case)
  • Handles the "special cases" (Date, Decimal, [String: Encodable]) by just returning them. See wrapEncodable and wrapUntyped

If you want [String: Any] rather than [String: Any?] (which is what ResultMap is), then you can tweak the types a bit. The only tricky piece is you would need to store something like nil as Any? as Any in order to encode nil (or you could encode NSNull, or you could just not encode it at all if you wanted).

Note that this actually returns Any, since it can't know that the top level encodes an object. So you'll need to as? cast it to [String: Any?].


To your question about using Mirror, the good thing about Mirror is that the code is short. The bad thing is that mirror is very slow. So it depends on how important that is. Not everything has the mirror you expect, however. For your purposes, Date has a "struct-like" Mirror, so you have to special-case it. But it's not that hard to write the code. Something like this:

func resultMap(from object: Any) -> Any {

    // First handle special cases that aren't what they seem
    if object is Date || object is Decimal {
        return object
    }

    let mirror = Mirror(reflecting: object)

    switch mirror.displayStyle {
    case .some(.struct), .some(.class), .some(.dictionary):
        var keyValues: [String: Any] = [:]
        for child in mirror.children {
            if let label = child.label {
                keyValues[label] = resultMap(from: child.value)
            }
        }
        return keyValues

    case .some(.collection):
        var values: [Any] = []
        for child in mirror.children {
            values.append(resultMap(from: child.value))
        }
        return values

    default:
        return object
    }
}

let animal = MockAnimalObject(teeth: MockTeethObject(numberOfTeeth: 10, dateOfTeethCreation: .now))
let result = resultMap(from: animal)
print(result)
// ["teeth": ["dateOfTeethCreation": 2022-08-12 21:08:11 +0000, "numberOfTeeth": 10]]

This time I didn't bother with Any?, but you could probably expand it that way if you needed. You'd need to decide what you'd want to do about enums, tuples, and anything else you'd want to handle specially, but it's pretty flexible. Just slow.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thank you for taking the time to create the code blob. Since I won't be using json at all -- thoughts on using mirror as an alternative to encoding? https://developer.apple.com/documentation/swift/mirror – Mocha Aug 12 '22 at 20:15
  • 1
    Mirror is extremely slow. https://twitter.com/kyleve/status/1557906275299536896 But you can give it a try. It likely can turn things into `[String: Any]`. The only question is whether all your types will mirror the way you want them to. Like Encodable, Mirror allows customization, which is very good, but also might behave in ways you don't like. For most cases it's probably going to work, just slowly. – Rob Napier Aug 12 '22 at 20:43
1

As pointed out in comments, JSON (Javascript object notation) is universal format and is not anyhow related to Date object in Swift after it is encoded. Therefore somewhere in the flow you need to make it String Double or some other object type that can be encoded to JSON. Anyway, if you want to make encoding Date to be easier you can take hold of some native JSONEncoder functions, such as folowing:

    let encoder = JSONEncoder()
    encoder.dateEncodingStrategy = .iso8601
Mr.SwiftOak
  • 1,469
  • 3
  • 8
  • 19
  • I have edited my question to be a bit more specific why I want to use JSON. I actually just need a generic [String:Any] dictionary. But I am using JSON as an easy class to dictionary conversion. – Mocha Aug 12 '22 at 16:34