What's the proper syntax of mapping/filtering a data-stream element to convert its data type?

早过忘川 提交于 2021-02-11 14:10:59

问题


Scenario: Data stream that contains an item that has changed from an Int to String type causing the JSON parser to crash.

Result: Subscriber 'sink' crashed with data type not matching the original receiving type via JSON parser.

Goal: to convert the Int values to String to have a consistent stream for a successful parsing.

Here's a snippet of the data stream that has caused the crash:

...
{
  "city": "אלון שבות",
  "sickCount": 124,
  "actualSick": 15,
  "verifiedLast7Days": " 11-14 ",
  "testLast7Days": 699,
  "patientDiffPopulationForTenThousands": 47
},
{
  "city": "סייד (שבט)",
  "sickCount": " קטן מ-15 ",
  "actualSick": " קטן מ-15 ",
  "verifiedLast7Days": "  0  ",
  "testLast7Days": 17,
  "patientDiffPopulationForTenThousands": 4
},
...

Here's the error via console:

CodingKeys(stringValue: "sickCount", intValue: nil)], debugDescription: "Expected to decode Int but found a string/data instead.", underlyingError: nil))

Here's the code:

func getData() {
    let str = "https://disease.sh/v3/covid-19/gov/Israel"
    let url = URL(string: str)!
    let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: url)
        .map(\.data)
        .receive(on: DispatchQueue.main)
        .decode(type: IsraelDataElement.self, decoder: JSONDecoder())
    
    remoteDataPublisher
        .eraseToAnyPublisher()
        .sink(receiveCompletion: { completion in
            switch completion {
            case .finished:
                print("{IsraelModel} Publisher Finished")
            case let .failure(anError):
                Swift.print("\nIsrael - Received error: #function", anError)
            }
        }, receiveValue: { someData in
            self.add_UUID(origData: someData)
            print(someData)
        }).store(in: &cancellables)
}

回答1:


I wasn't sure if what I was suggesting in comments wasn't clear, so here's an example of what I had in mind.

If the object you're trying to decode is:

struct IsraelDataElement {
  let city: String
  let sickCount: String,
  let actualSick: String,
  let verifiedLast7Days: String,
  let testLast7Days: Int,
  let patientDiffPopulationForTenThousands: Int
}

then you can manually decode it, converting renegade Ints to Strings:

extension IsraelDataElement: Decodable {
   private enum CodingKeys: CodingKey {
      case city, sickCount, actualSick, verifiedLast7Days //... etc
   }
   init(from decoder: Decoder) throws {
      let container = try decoder.container(keyedBy: CodingKeys.self)

      self.city = try container.decode(String.self, forKey: .city)
      do {
         self.sickCount = try String(container.decode(Int.self, forKey: .sickCount))
      } catch DecodingError.typeMismatch {
         self.sickCount try container.decode(String.self, forKey: .sickCount)
      }
      // and so on for other properties
   } 
}

Then, no further changes are needed in your Combine chain.




回答2:


Swift strongly typed so it will not coerce the types for you. If you genuinely mean for the types to be heterogenous then idiomatically you should use an enum to represent that this thing is either an Int or String. This is the only way you can actually round trip the data and preserver the type. For instance:

struct Element: Decodable {
  let city: String
  let sickCount: Either<String, Int>
  let actualSick: Either<String, Int>
  let verifiedLast7Days: String
  let testLast7Days: Int
  let patientDiffPopulationForTenThousands: Int
}

enum Either<Left: Decodable, Right: Decodable>: Decodable {
    case left(Left)
    case right(Right)

  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    if let x = try? container.decode(Left.self) {
      self = .left(x)
    } else if let x = try? container.decode(Right.self) {
      self = .right(x)
    } else {
      throw DecodingError.typeMismatch(Self.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for \(String(describing:Self.self))"))
    }
  }
}

If this is just a case of your backend developers being sloppy then convert the type yourself and you can do so generically so you can easily fix their data problems everywhere in your app:

struct Element: Decodable {
  let city: String
  let sickCount: Int
  let actualSick: Int

  enum CodingKeys: CodingKey {
    case city, sickCount, actualSick
  }

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    self.city = try container.decode(String.self, forKey: .city)
    self.sickCount = try container.decodeAndCoerce(to: Int.self, from: String.self, conversion: Int.init(_:), forKey: .sickCount)
    self.actualSick = try container.decodeAndCoerce(to: Int.self, from: String.self, conversion: Int.init(_:), forKey: .actualSick)
  }
}

extension KeyedDecodingContainer {
  func decodeAndCoerce<Target: Decodable, Source: Decodable>(to: Target.Type, from: Source.Type, conversion: @escaping (Source) -> Target?, forKey key: Key) throws -> Target {
    guard let value = (try? decode(Target.self, forKey: key)) ?? ((try? decode(Source.self, forKey: key)).flatMap(conversion)) else {
      throw DecodingError.typeMismatch(Target.self,  DecodingError.Context(codingPath: codingPath, debugDescription: "Expected \(String(describing: Target.self))"))
    }
    return value
  }
}

Aren't generics fun?



来源:https://stackoverflow.com/questions/65649602/whats-the-proper-syntax-of-mapping-filtering-a-data-stream-element-to-convert-i

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!