//
// PythonDecoder.swift
// PythonCodable
//
// Created by Pedro José Pereira Vieito on 11/12/2019.
// Copyright © 2019 Pedro José Pereira Vieito. All rights reserved.
//
import Foundation
import PythonKit
import MoreCodable
public struct PythonDecoder {
/// Tries to decodes the input `pythonObject` into the Swift `type` type.
/// - Parameters:
/// - type: Decodable Swift type.
/// - pythonObject: Input Python object.
public static func decode(_ type: T.Type, from pythonObject: PythonObject) throws -> T {
let decodableDictionary = try pythonObject.bridgeToDecodableDictionary()
return try DictionaryDecoder().decode(type, from: decodableDictionary)
}
}
extension PythonObject {
/// Tries to bridge the `PythonObject` into an equivalent Swift type.
public func bridgeToSwift() throws -> Any? {
return try self.bridgeToDecodableValue()
}
}
extension PythonDecoder {
enum Error: Swift.Error {
case unsupportedType
case unsupportedListType
case unsupportedDictionaryType
}
}
extension PythonObject {
func bridgeToDecodableDictionaryKey() throws -> String {
guard self.isPythonString else {
throw PythonError.invalidCall(self)
}
return String(self)!
}
func bridgeToDecodableValue() throws -> Any? {
if self.isPythonString {
return String(self)!
}
else if self.isPythonBool {
return Bool(self)!
}
else if self.isPythonInteger {
return Int(self)!
}
else if self.isPythonFloat {
return Double(self)!
}
else if self.isPythonNone {
return nil
}
// Try dictionary before list, since dictionaries are list convertible.
else if self.isPythonDictionaryOrDictionaryConvertible {
return try self.bridgeToDecodableDictionary()
}
else if self.isPythonListOrListConvertible {
return try self.bridgeToDecodableArray()
}
else {
throw PythonDecoder.Error.unsupportedType
}
}
func bridgeToDecodableArray() throws -> Array {
let pythonList = try self.convertToPythonList()
var bridgedArray: [Any?] = []
for item in pythonList {
let bridgedValue = try item.bridgeToDecodableValue()
bridgedArray.append(bridgedValue)
}
return bridgedArray
}
func bridgeToDecodableDictionary() throws -> Dictionary {
let pythonDictionary = try self.convertToPythonDictionary()
var bridgedDictionary: [String : Any] = [:]
for item in pythonDictionary.items() {
let (key, value) = item.tuple2
if key.isPythonString, let bridgedValue = try value.bridgeToDecodableValue() {
bridgedDictionary[String(key)!] = bridgedValue
}
}
return bridgedDictionary
}
}
extension PythonObject {
var isPythonListOrListConvertible: Bool {
return self.isPythonList || self.isPythonListConvertible
}
var isPythonListConvertible: Bool {
return Bool(Python.hasattr(self, "__iter__"))!
}
func convertToPythonList() throws -> PythonObject {
if self.isPythonList {
return self
}
else if self.isPythonListConvertible {
return Python.list(pythonObject)
}
else {
throw PythonDecoder.Error.unsupportedListType
}
}
}
extension PythonObject {
var isPythonDictionaryOrDictionaryConvertible: Bool {
return self.isPythonDictionary || self.isPythonDictionaryConvertible
}
var isPythonNamedTupleConvertible: Bool {
return Bool(Python.hasattr(self, "_asdict"))!
}
var isPythonDictionaryConvertible: Bool {
return Bool(Python.hasattr(self, "__dict__"))!
}
func convertToPythonDictionary() throws -> PythonObject {
if self.isPythonDictionary {
return self
}
else if self.isPythonDictionaryConvertible {
return Python.vars(pythonObject)
}
else if self.isPythonNamedTupleConvertible {
return Python.dict(pythonObject._asdict())
}
else {
throw PythonDecoder.Error.unsupportedDictionaryType
}
}
}
extension PythonObject {
var isPythonString: Bool {
return Bool(Python.isinstance(pythonObject, Python.str))!
}
var isPythonInteger: Bool {
return Bool(Python.isinstance(pythonObject, Python.int))!
}
var isPythonFloat: Bool {
return Bool(Python.isinstance(pythonObject, Python.float))!
}
var isPythonBool: Bool {
return Bool(Python.isinstance(pythonObject, Python.bool))!
}
var isPythonList: Bool {
return Bool(Python.isinstance(pythonObject, Python.list))!
}
var isPythonDictionary: Bool {
return Bool(Python.isinstance(pythonObject, Python.dict))!
}
var isPythonNone: Bool {
return Bool(Python.isinstance(pythonObject, Python.type(Python.None)))!
}
}