diff --git a/Package.resolved b/Package.resolved index f826d5b..a27819f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/omochi/TypeScriptAST", "state" : { - "revision" : "a6224e4764bf4bd1b7eee9aecf4e93722ceaf64c", - "version" : "1.0.0" + "revision" : "b846cf79f6e044c64d257823a9b5a1266114137a", + "version" : "1.1.0" } } ], diff --git a/Package.swift b/Package.swift index 441e17f..828f15b 100644 --- a/Package.swift +++ b/Package.swift @@ -12,7 +12,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/omochi/SwiftTypeReader", from: "2.1.1"), - .package(url: "https://github.com/omochi/TypeScriptAST", from: "1.0.0") + .package(url: "https://github.com/omochi/TypeScriptAST", from: "1.1.0") ], targets: [ .target( diff --git a/Sources/CodableToTypeScript/Extensions/STypeEx.swift b/Sources/CodableToTypeScript/Extensions/STypeEx.swift index 1177a92..6fed2cd 100644 --- a/Sources/CodableToTypeScript/Extensions/STypeEx.swift +++ b/Sources/CodableToTypeScript/Extensions/STypeEx.swift @@ -105,11 +105,6 @@ extension SType { guard let self = self.asNominal else { return false } return self.nominalTypeDecl.isStandardLibraryType(name) } - - func hasStringRawValue() -> Bool { - guard let self = self.asNominal else { return false } - return self.nominalTypeDecl.hasStringRawValue() - } } extension ParamDecl { diff --git a/Sources/CodableToTypeScript/Generator/CodeGenerator.swift b/Sources/CodableToTypeScript/Generator/CodeGenerator.swift index 6118d23..f9d1209 100644 --- a/Sources/CodableToTypeScript/Generator/CodeGenerator.swift +++ b/Sources/CodableToTypeScript/Generator/CodeGenerator.swift @@ -4,9 +4,9 @@ import TypeScriptAST public final class CodeGenerator { internal final class RequestToken: HashableFromIdentity { - unowned let gen: CodeGenerator - init(gen: CodeGenerator) { - self.gen = gen + unowned let generator: CodeGenerator + init(generator: CodeGenerator) { + self.generator = generator } } @@ -20,7 +20,7 @@ public final class CodeGenerator { ) { self.context = context self.typeConverterProvider = typeConverterProvider - self.requestToken = RequestToken(gen: self) + self.requestToken = RequestToken(generator: self) } public func converter(for type: any SType) throws -> any TypeConverter { @@ -40,11 +40,11 @@ public final class CodeGenerator { internal struct ConverterRequest: Request { var token: RequestToken @AnyTypeStorage var type: any SType - private var gen: CodeGenerator { token.gen } + private var generator: CodeGenerator { token.generator } func evaluate(on evaluator: RequestEvaluator) throws -> any TypeConverter { - let impl = try gen.implConverter(for: type) - return GeneratorProxyConverter(generator: gen, type: type, impl: impl) + let impl = try generator.implConverter(for: type) + return GeneratorProxyConverter(generator: generator, type: type, impl: impl) } } @@ -54,7 +54,7 @@ public final class CodeGenerator { func evaluate(on evaluator: RequestEvaluator) throws -> Bool { do { - let converter = try token.gen.implConverter(for: type) + let converter = try token.generator.implConverter(for: type) return try converter.hasDecode() } catch { switch error { @@ -65,6 +65,23 @@ public final class CodeGenerator { } } + internal struct HasEncodeRequest: Request { + var token: RequestToken + @AnyTypeStorage var type: any SType + + func evaluate(on evaluator: RequestEvaluator) throws -> Bool { + do { + let converter = try token.generator.implConverter(for: type) + return try converter.hasEncode() + } catch { + switch error { + case is CycleRequestError: return true + default: throw error + } + } + } + } + func helperLibrary() -> HelperLibraryGenerator { return HelperLibraryGenerator(generator: self) } @@ -87,4 +104,19 @@ public final class CodeGenerator { return TSCallExpr(callee: callee, args: args) } + + public func callEncode( + callee: any TSExpr, + genericArgs: [any SType], + entity: any TSExpr + ) throws -> any TSExpr { + var args: [any TSExpr] = [entity] + + for arg in genericArgs { + let encode = try converter(for: arg).boundEncode() + args.append(encode) + } + + return TSCallExpr(callee: callee, args: args) + } } diff --git a/Sources/CodableToTypeScript/Generator/HelperLibraryGenerator.swift b/Sources/CodableToTypeScript/Generator/HelperLibraryGenerator.swift index 123ba9a..101728d 100644 --- a/Sources/CodableToTypeScript/Generator/HelperLibraryGenerator.swift +++ b/Sources/CodableToTypeScript/Generator/HelperLibraryGenerator.swift @@ -2,21 +2,19 @@ import TypeScriptAST struct HelperLibraryGenerator { enum EntryKind: CaseIterable { - case identityFunction - case optionalFieldDecodeFunction - case optionalDecodeFunction - case arrayDecodeFunction - case dictionaryDecodeFunction + case identity + case optionalFieldDecode + case optionalFieldEncode + case optionalDecode + case optionalEncode + case arrayDecode + case arrayEncode + case dictionaryDecode + case dictionaryEncode } var generator: CodeGenerator - let identityFunctionName = "identity" - let optionalFieldDecodeFunctionName = "OptionalField_decode" - let optionalDecodeFunctionName = "Optional_decode" - let arrayDecodeFunctionName = "Array_decode" - let dictionaryDecodeFunctionName = "Dictionary_decode" - func generate() -> TSSourceFile { var decls: [any ASTNode] = [] @@ -29,11 +27,15 @@ struct HelperLibraryGenerator { func name(_ entry: EntryKind) -> String { switch entry { - case .identityFunction: return "identity" - case .optionalFieldDecodeFunction: return "OptionalField_decode" - case .optionalDecodeFunction: return "Optional_decode" - case .arrayDecodeFunction: return "Array_decode" - case .dictionaryDecodeFunction: return "Dictionary_decode" + case .identity: return "identity" + case .optionalFieldDecode: return "OptionalField_decode" + case .optionalFieldEncode: return "OptionalField_encode" + case .optionalDecode: return "Optional_decode" + case .optionalEncode: return "Optional_encode" + case .arrayDecode: return "Array_decode" + case .arrayEncode: return "Array_encode" + case .dictionaryDecode: return "Dictionary_decode" + case .dictionaryEncode: return "Dictionary_encode" } } @@ -43,7 +45,7 @@ struct HelperLibraryGenerator { func decl(_ entry: EntryKind) -> any TSDecl { switch entry { - case .identityFunction: + case .identity: let decl = TSFunctionDecl( modifiers: [.export], name: name(entry), @@ -55,16 +57,16 @@ struct HelperLibraryGenerator { ]) ) return decl - case .optionalFieldDecodeFunction: + case .optionalFieldDecode: let decl = TSFunctionDecl( modifiers: [.export], name: name(entry), - genericParams: ["T", "U"], + genericParams: ["T", "T_JSON"], params: [ - .init(name: "json", type: TSUnionType([TSIdentType("T"), TSIdentType.undefined])), - tDecoderParameter() + .init(name: "json", type: TSUnionType([TSIdentType("T_JSON"), TSIdentType.undefined])), + tDecodeParameter() ], - result: TSUnionType([TSIdentType("U"), TSIdentType.undefined]), + result: TSUnionType([TSIdentType("T"), TSIdentType.undefined]), body: TSBlockStmt([ TSIfStmt( condition: TSInfixOperatorExpr( @@ -72,20 +74,41 @@ struct HelperLibraryGenerator { ), then: TSReturnStmt(TSIdentExpr.undefined) ), - TSReturnStmt(callTDecoder()) + TSReturnStmt(callTDecode()) ]) ) return decl - case .optionalDecodeFunction: + case .optionalFieldEncode: let decl = TSFunctionDecl( modifiers: [.export], name: name(entry), - genericParams: [.init("T"), .init("U")], + genericParams: ["T", "T_JSON"], params: [ - .init(name: "json", type: TSUnionType([TSIdentType("T"), TSIdentType.null])), - tDecoderParameter() + .init(name: "entity", type: TSUnionType([TSIdentType("T"), TSIdentType.undefined])), + tEncodeParameter() ], - result: TSUnionType([TSIdentType("U"), TSIdentType.null]), + result: TSUnionType([TSIdentType("T_JSON"), TSIdentType.undefined]), + body: TSBlockStmt([ + TSIfStmt( + condition: TSInfixOperatorExpr( + TSIdentExpr("entity"), "===", TSIdentExpr.undefined + ), + then: TSReturnStmt(TSIdentExpr.undefined) + ), + TSReturnStmt(callTEncode()) + ]) + ) + return decl + case .optionalDecode: + let decl = TSFunctionDecl( + modifiers: [.export], + name: name(entry), + genericParams: [.init("T"), .init("T_JSON")], + params: [ + .init(name: "json", type: TSUnionType([TSIdentType("T_JSON"), TSIdentType.null])), + tDecodeParameter() + ], + result: TSUnionType([TSIdentType("T"), TSIdentType.null]), body: TSBlockStmt([ TSIfStmt( condition: TSInfixOperatorExpr( @@ -93,47 +116,88 @@ struct HelperLibraryGenerator { ), then: TSReturnStmt(TSIdentExpr.null) ), - TSReturnStmt(callTDecoder()) + TSReturnStmt(callTDecode()) + ]) + ) + return decl + case .optionalEncode: + let decl = TSFunctionDecl( + modifiers: [.export], + name: name(entry), + genericParams: [.init("T"), .init("T_JSON")], + params: [ + .init(name: "entity", type: TSUnionType([TSIdentType("T"), TSIdentType.null])), + tEncodeParameter() + ], + result: TSUnionType([TSIdentType("T_JSON"), TSIdentType.null]), + body: TSBlockStmt([ + TSIfStmt( + condition: TSInfixOperatorExpr( + TSIdentExpr("entity"), "===", TSIdentExpr.null + ), + then: TSReturnStmt(TSIdentExpr.null) + ), + TSReturnStmt(callTEncode()) ]) ) return decl - case .arrayDecodeFunction: + case .arrayDecode: let decl = TSFunctionDecl( modifiers: [.export], name: name(entry), - genericParams: ["T", "U"], + genericParams: ["T", "T_JSON"], params: [ - .init(name: "json", type: TSArrayType(TSIdentType("T"))), - tDecoderParameter() + .init(name: "json", type: TSArrayType(TSIdentType("T_JSON"))), + tDecodeParameter() ], - result: TSArrayType(TSIdentType("U")), + result: TSArrayType(TSIdentType("T")), body: TSBlockStmt([ TSReturnStmt( TSCallExpr( callee: TSMemberExpr( base: TSIdentExpr("json"), name: TSIdentExpr("map") ), - args: [ - tDecoderName() - ] + args: [tDecode()] ) ) ]) ) return decl - case .dictionaryDecodeFunction: + case .arrayEncode: let decl = TSFunctionDecl( modifiers: [.export], name: name(entry), - genericParams: ["T", "U"], + genericParams: ["T", "T_JSON"], params: [ - .init(name: "json", type: TSDictionaryType(TSIdentType("T"))), - tDecoderParameter() + .init(name: "entity", type: TSArrayType(TSIdentType("T"))), + tEncodeParameter() ], - result: TSDictionaryType(TSIdentType("U")), + result: TSArrayType(TSIdentType("T_JSON")), + body: TSBlockStmt([ + TSReturnStmt( + TSCallExpr( + callee: TSMemberExpr( + base: TSIdentExpr("entity"), name: TSIdentExpr("map") + ), + args: [tEncode()] + ) + ) + ]) + ) + return decl + case .dictionaryDecode: + let decl = TSFunctionDecl( + modifiers: [.export], + name: name(entry), + genericParams: ["T", "T_JSON"], + params: [ + .init(name: "json", type: TSDictionaryType(TSIdentType("T_JSON"))), + tDecodeParameter() + ], + result: TSDictionaryType(TSIdentType("T")), body: TSBlockStmt([ TSVarDecl( - kind: .const, name: "result", type: TSDictionaryType(TSIdentType("U")), + kind: .const, name: "entity", type: TSDictionaryType(TSIdentType("T")), initializer: TSObjectExpr([]) ), TSForInStmt( @@ -148,9 +212,9 @@ struct HelperLibraryGenerator { ), then: TSBlockStmt([ TSAssignExpr( - TSSubscriptExpr(base: TSIdentExpr("result"), key: TSIdentExpr("k")), + TSSubscriptExpr(base: TSIdentExpr("entity"), key: TSIdentExpr("k")), TSCallExpr( - callee: tDecoderName(), + callee: tDecode(), args: [ TSSubscriptExpr(base: TSIdentExpr("json"), key: TSIdentExpr("k")) ] @@ -160,33 +224,99 @@ struct HelperLibraryGenerator { ) ]) ), - TSReturnStmt(TSIdentExpr("result")) + TSReturnStmt(TSIdentExpr("entity")) + ]) + ) + return decl + case .dictionaryEncode: + let decl = TSFunctionDecl( + modifiers: [.export], + name: name(entry), + genericParams: ["T", "T_JSON"], + params: [ + .init(name: "entity", type: TSDictionaryType(TSIdentType("T"))), + tEncodeParameter() + ], + result: TSDictionaryType(TSIdentType("T_JSON")), + body: TSBlockStmt([ + TSVarDecl( + kind: .const, name: "json", type: TSDictionaryType(TSIdentType("T_JSON")), + initializer: TSObjectExpr([]) + ), + TSForInStmt( + kind: .const, name: "k", operator: .in, expr: TSIdentExpr("entity"), + body: TSBlockStmt([ + TSIfStmt( + condition: TSCallExpr( + callee: TSMemberExpr( + base: TSIdentExpr("entity"), name: TSIdentExpr("hasOwnProperty") + ), + args: [TSIdentExpr("k")] + ), + then: TSBlockStmt([ + TSAssignExpr( + TSSubscriptExpr(base: TSIdentExpr("json"), key: TSIdentExpr("k")), + TSCallExpr( + callee: tEncode(), + args: [ + TSSubscriptExpr(base: TSIdentExpr("entity"), key: TSIdentExpr("k")) + ] + ) + ) + ]) + ) + ]) + ), + TSReturnStmt(TSIdentExpr("json")) ]) ) return decl } } - private func tDecoderName() -> TSIdentExpr { + private func tDecode() -> TSIdentExpr { return TSIdentExpr( DefaultTypeConverter.decodeName(entityName: "T") ) } - private func tDecoderParameter() -> TSFunctionType.Param { + private func tDecodeParameter() -> TSFunctionType.Param { return TSFunctionType.Param( - name: tDecoderName().name, + name: tDecode().name, type: TSFunctionType( - params: [.init(name: "json", type: TSIdentType("T"))], - result: TSIdentType("U") + params: [.init(name: "json", type: TSIdentType("T_JSON"))], + result: TSIdentType("T") ) ) } - private func callTDecoder() -> any TSExpr { + private func callTDecode() -> any TSExpr { return TSCallExpr( - callee: tDecoderName(), + callee: tDecode(), args: [TSIdentExpr("json")] ) } + + private func tEncode() -> TSIdentExpr { + return TSIdentExpr( + DefaultTypeConverter.encodeName(entityName: "T") + ) + } + + private func tEncodeParameter() -> TSFunctionType.Param { + return TSFunctionType.Param( + name: tEncode().name, + type: TSFunctionType( + params: [.init(name: "entity", type: TSIdentType("T"))], + result: TSIdentType("T_JSON") + ) + ) + } + + private func callTEncode() -> any TSExpr { + return TSCallExpr( + callee: tEncode(), + args: [TSIdentExpr("entity")] + ) + } } diff --git a/Sources/CodableToTypeScript/TypeConverter/ArrayConverter.swift b/Sources/CodableToTypeScript/TypeConverter/ArrayConverter.swift index f2e6a7d..2a7f6c5 100644 --- a/Sources/CodableToTypeScript/TypeConverter/ArrayConverter.swift +++ b/Sources/CodableToTypeScript/TypeConverter/ArrayConverter.swift @@ -21,16 +21,28 @@ struct ArrayConverter: TypeConverter { } func decodeName() throws -> String? { - return generator.helperLibrary().name(.arrayDecodeFunction) + return generator.helperLibrary().name(.arrayDecode) } - func callDecode(json: TSExpr) throws -> TSExpr { - guard try hasDecode() else { return json } - let decodeName = try decodeName().unwrap(name: "decode name") - return try generator.callDecode( - callee: TSIdentExpr(decodeName), + func callDecode(json: any TSExpr) throws -> any TSExpr { + return try `default`.callDecode( genericArgs: [try element().type], json: json ) } + + func hasEncode() throws -> Bool { + return try element().hasEncode() + } + + func encodeName() throws -> String { + return generator.helperLibrary().name(.arrayEncode) + } + + func callEncode(entity: any TSExpr) throws -> any TSExpr { + return try `default`.callEncode( + genericArgs: [try element().type], + entity: entity + ) + } } diff --git a/Sources/CodableToTypeScript/TypeConverter/DefaultTypeConverter.swift b/Sources/CodableToTypeScript/TypeConverter/DefaultTypeConverter.swift index 96fef2a..6a33527 100644 --- a/Sources/CodableToTypeScript/TypeConverter/DefaultTypeConverter.swift +++ b/Sources/CodableToTypeScript/TypeConverter/DefaultTypeConverter.swift @@ -72,7 +72,7 @@ public struct DefaultTypeConverter { public func decodeName() throws -> String { let converter = try self.converter() guard try converter.hasDecode() else { - throw MessageError("no decoder") + throw MessageError("no decode") } let entityName = try converter.name(for: .entity) return Self.decodeName(entityName: entityName) @@ -86,7 +86,7 @@ public struct DefaultTypeConverter { let converter = try self.converter() guard try converter.hasDecode() else { - return generator.helperLibrary().access(.identityFunction) + return generator.helperLibrary().access(.identity) } func makeClosure() throws -> any TSExpr { @@ -114,6 +114,10 @@ public struct DefaultTypeConverter { } public func callDecode(json: any TSExpr) throws -> any TSExpr { + return try callDecode(genericArgs: type.genericArgs, json: json) + } + + public func callDecode(genericArgs: [any SType], json: any TSExpr) throws -> any TSExpr { let converter = try self.converter() guard try converter.hasDecode() else { return json @@ -121,7 +125,7 @@ public struct DefaultTypeConverter { let decodeName = try converter.decodeName() return try generator.callDecode( callee: TSIdentExpr(decodeName), - genericArgs: type.genericArgs, + genericArgs: genericArgs, json: json ) } @@ -160,7 +164,7 @@ public struct DefaultTypeConverter { decodeGenericParams.append(try param.name(for: .json)) } - var parameters: [TSFunctionType.Param] = [ + var params: [TSFunctionType.Param] = [ .init( name: "json", type: TSIdentType(jsonName, genericArgs: jsonArgs) @@ -177,7 +181,7 @@ public struct DefaultTypeConverter { let decodeName = try param.decodeName() - parameters.append( + params.append( .init( name: decodeName, type: decodeType @@ -189,7 +193,7 @@ public struct DefaultTypeConverter { modifiers: [.export], name: try decodeName(), genericParams: decodeGenericParams, - params: parameters, + params: params, result: result, body: TSBlockStmt() ) @@ -199,4 +203,139 @@ public struct DefaultTypeConverter { guard let _ = try decodeSignature() else { return nil } throw MessageError("Unsupported type: \(type)") } + + public func encodeName() throws -> String { + let converter = try self.converter() + guard try converter.hasEncode() else { + throw MessageError("no encode") + } + let entityName = try converter.name(for: .entity) + return Self.encodeName(entityName: entityName) + } + + public static func encodeName(entityName: String) -> String { + return "\(entityName)_encode" + } + + public func boundEncode() throws -> any TSExpr { + let converter = try self.converter() + + guard try converter.hasEncode() else { + return generator.helperLibrary().access(.identity) + } + + func makeClosure() throws -> any TSExpr { + let param = TSFunctionType.Param( + name: "entity", + type: try converter.type(for: .entity) + ) + let result = try converter.type(for: .json) + let expr = try converter.callEncode(entity: TSIdentExpr("entity")) + return TSClosureExpr( + params: [param], + result: result, + body: TSBlockStmt([ + TSReturnStmt(expr) + ]) + ) + } + + if !type.genericArgs.isEmpty { + return try makeClosure() + } + return TSIdentExpr( + try converter.encodeName() + ) + } + + public func callEncode(entity: any TSExpr) throws -> any TSExpr { + return try callEncode(genericArgs: type.genericArgs, entity: entity) + } + + public func callEncode(genericArgs: [any SType], entity: any TSExpr) throws -> any TSExpr { + let converter = try self.converter() + guard try converter.hasEncode() else { + return entity + } + let encodeName = try converter.encodeName() + return try generator.callEncode( + callee: TSIdentExpr(encodeName), + genericArgs: genericArgs, + entity: entity + ) + } + + public func callEncodeField(entity: any TSExpr) throws -> any TSExpr { + return try converter().callEncode(entity: entity) + } + + public func encodeSignature() throws -> TSFunctionDecl? { + let converter = try self.converter() + + guard try converter.hasEncode() else { return nil } + + let entityName = try converter.name(for: .entity) + let jsonName = try converter.name(for: .json) + + let genericParams = try converter.genericParams() + + var entityArgs: [any TSType] = [] + var jsonArgs: [any TSType] = [] + + for param in genericParams { + entityArgs.append( + TSIdentType(try param.name(for: .entity)) + ) + jsonArgs.append( + TSIdentType(try param.name(for: .json)) + ) + } + + var encodeGenericParams: [String] = [] + for param in genericParams { + encodeGenericParams.append(try param.name(for: .entity)) + } + for param in genericParams { + encodeGenericParams.append(try param.name(for: .json)) + } + + var params: [TSFunctionType.Param] = [ + .init( + name: "entity", + type: TSIdentType(entityName, genericArgs: entityArgs) + ) + ] + let result: any TSType = TSIdentType(jsonName, genericArgs: jsonArgs) + + for param in genericParams { + let entityName = try param.name(for: .entity) + let encodeType: any TSType = TSFunctionType( + params: [.init(name: "entity", type: TSIdentType(entityName))], + result: TSIdentType(try param.name(for: .json)) + ) + + let encodeName = try param.encodeName() + + params.append( + .init( + name: encodeName, + type: encodeType + ) + ) + } + + return TSFunctionDecl( + modifiers: [.export], + name: try encodeName(), + genericParams: encodeGenericParams, + params: params, + result: result, + body: TSBlockStmt() + ) + } + + public func encodeDecl() throws -> TSFunctionDecl? { + guard let _ = try encodeSignature() else { return nil } + throw MessageError("Unsupported type: \(type)") + } } diff --git a/Sources/CodableToTypeScript/TypeConverter/DictionaryConverter.swift b/Sources/CodableToTypeScript/TypeConverter/DictionaryConverter.swift index b6b219f..521a77d 100644 --- a/Sources/CodableToTypeScript/TypeConverter/DictionaryConverter.swift +++ b/Sources/CodableToTypeScript/TypeConverter/DictionaryConverter.swift @@ -21,16 +21,28 @@ struct DictionaryConverter: TypeConverter { } func decodeName() throws -> String? { - return generator.helperLibrary().name(.dictionaryDecodeFunction) + return generator.helperLibrary().name(.dictionaryDecode) } - func callDecode(json: TSExpr) throws -> TSExpr { - guard try hasDecode() else { return json } - let decodeName = try decodeName().unwrap(name: "decode name") - return try generator.callDecode( - callee: TSIdentExpr(decodeName), + func callDecode(json: any TSExpr) throws -> any TSExpr { + return try `default`.callDecode( genericArgs: [try value().type], json: json ) } + + func hasEncode() throws -> Bool { + return try value().hasEncode() + } + + func encodeName() throws -> String { + return generator.helperLibrary().name(.dictionaryEncode) + } + + func callEncode(entity: any TSExpr) throws -> any TSExpr { + return try `default`.callEncode( + genericArgs: [try value().type], + entity: entity + ) + } } diff --git a/Sources/CodableToTypeScript/TypeConverter/EnumConverter.swift b/Sources/CodableToTypeScript/TypeConverter/EnumConverter.swift index 54bdfe0..7ae4bdc 100644 --- a/Sources/CodableToTypeScript/TypeConverter/EnumConverter.swift +++ b/Sources/CodableToTypeScript/TypeConverter/EnumConverter.swift @@ -7,6 +7,8 @@ struct EnumConverter: TypeConverter { var type: any SType { `enum` } + private var decl: EnumDecl { `enum`.decl } + func typeDecl(for target: GenerationTarget) throws -> TSTypeDecl? { switch target { case .entity: break @@ -16,15 +18,15 @@ struct EnumConverter: TypeConverter { let genericParams = try self.genericParams().map { try $0.name(for: target) } - if `enum`.decl.caseElements.isEmpty { + if decl.caseElements.isEmpty { return TSTypeDecl( modifiers: [.export], name: try name(for: target), genericParams: genericParams, type: TSIdentType.never ) - } else if `enum`.decl.hasStringRawValue() { - let items: [any TSType] = `enum`.decl.caseElements.map { (ce) in + } else if decl.hasStringRawValue() { + let items: [any TSType] = decl.caseElements.map { (ce) in TSStringLiteralType(ce.name) } @@ -36,7 +38,7 @@ struct EnumConverter: TypeConverter { ) } - let items: [any TSType] = try `enum`.decl.caseElements.map { (ce) in + let items: [any TSType] = try decl.caseElements.map { (ce) in try transpile(caseElement: ce, target: target) } @@ -87,13 +89,13 @@ struct EnumConverter: TypeConverter { } func hasDecode() throws -> Bool { - if `enum`.decl.caseElements.isEmpty { + if decl.caseElements.isEmpty { return false - } else if `enum`.hasStringRawValue() { + } else if decl.hasStringRawValue() { return false - } else { - return true } + + return true } func decodeDecl() throws -> TSFunctionDecl? { @@ -103,6 +105,33 @@ struct EnumConverter: TypeConverter { type: `enum`.decl ).generate() } + + func hasEncode() throws -> Bool { + if decl.caseElements.isEmpty { + return false + } else if decl.hasStringRawValue() { + return false + } + + for caseElement in decl.caseElements { + for value in caseElement.associatedValues { + let value = try generator.converter(for: value.interfaceType) + if try value.hasEncode() { + return true + } + } + } + + return false + } + + func encodeDecl() throws -> TSFunctionDecl? { + return try EncodeFuncGen( + generator: generator, + converter: self, + type: `enum`.decl + ).generate() + } } private struct DecodeFuncGen { @@ -226,3 +255,81 @@ private struct DecodeFuncGen { return decl } } + +private struct EncodeFuncGen { + var generator: CodeGenerator + var converter: EnumConverter + var type: EnumDecl + + func encodeCaseValue(element: EnumCaseElementDecl) throws -> TSObjectExpr { + var fields: [TSObjectExpr.Field] = [] + + for value in element.associatedValues { + var expr: any TSExpr = TSMemberExpr(base: TSIdentExpr("e"), name: TSIdentExpr(value.codableLabel)) + + expr = try generator.converter(for: value.interfaceType).callEncodeField(entity: expr) + + fields.append(.init( + name: value.codableLabel, + value: expr + )) + } + + return TSObjectExpr(fields) + } + + func caseBody(element: EnumCaseElementDecl) throws -> [any ASTNode] { + var code: [any ASTNode] = [] + + if !element.associatedValues.isEmpty { + let e = TSVarDecl( + kind: .const, name: "e", + initializer: TSMemberExpr(base: TSIdentExpr("entity"), name: TSIdentExpr(element.name)) + ) + code.append(e) + } + + let innerObject = try encodeCaseValue(element: element) + + let outerObject = TSObjectExpr([ + .init(name: element.name, value: innerObject) + ]) + + code.append(TSReturnStmt(outerObject)) + return code + } + + func generate() throws -> TSFunctionDecl? { + guard let decl = try converter.encodeSignature() else { return nil } + + let `switch` = TSSwitchStmt( + expr: TSMemberExpr(base: TSIdentExpr("entity"), name: TSIdentExpr("kind")) + ) + + for caseElement in type.caseElements { + `switch`.cases.append( + TSCaseStmt(expr: TSStringLiteralExpr(caseElement.name), elements: [ + TSBlockStmt(try caseBody(element: caseElement)) + ]) + ) + } + + `switch`.cases.append( + TSDefaultStmt(elements: [ + TSVarDecl( + kind: .const, name: "check", type: TSIdentType.never, + initializer: TSIdentExpr("entity") + ), + TSThrowStmt(TSNewExpr(callee: TSIdentType.error, args: [ + TSInfixOperatorExpr( + TSStringLiteralExpr("invalid case: "), "+", + TSIdentExpr("check") + ) + ])) + ]) + ) + + decl.body.elements.append(`switch`) + return decl + } +} diff --git a/Sources/CodableToTypeScript/TypeConverter/ErrorTypeConverter.swift b/Sources/CodableToTypeScript/TypeConverter/ErrorTypeConverter.swift index 654ecbe..2087cfb 100644 --- a/Sources/CodableToTypeScript/TypeConverter/ErrorTypeConverter.swift +++ b/Sources/CodableToTypeScript/TypeConverter/ErrorTypeConverter.swift @@ -7,4 +7,8 @@ struct ErrorTypeConverter: TypeConverter { func hasDecode() throws -> Bool { throw MessageError("Error type can't be evaluated: \(type)") } + + func hasEncode() throws -> Bool { + throw MessageError("Error type can't be evaluated: \(type)") + } } diff --git a/Sources/CodableToTypeScript/TypeConverter/GeneratorProxyConverter.swift b/Sources/CodableToTypeScript/TypeConverter/GeneratorProxyConverter.swift index 0b467a5..ad29e25 100644 --- a/Sources/CodableToTypeScript/TypeConverter/GeneratorProxyConverter.swift +++ b/Sources/CodableToTypeScript/TypeConverter/GeneratorProxyConverter.swift @@ -52,4 +52,35 @@ struct GeneratorProxyConverter: TypeConverter { func decodeDecl() throws -> TSFunctionDecl? { return try impl.decodeDecl() } + + func hasEncode() throws -> Bool { + return try generator.context.evaluator( + CodeGenerator.HasEncodeRequest(token: generator.requestToken, type: type) + ) + } + + func encodeName() throws -> String { + return try impl.encodeName() + } + + func boundEncode() throws -> TSExpr { + return try impl.boundEncode() + } + + func callEncode(entity: any TSExpr) throws -> any TSExpr { + return try impl.callEncode(entity: entity) + } + + func callEncodeField(entity: any TSExpr) throws -> any TSExpr { + return try impl.callEncodeField(entity: entity) + } + + func encodeSignature() throws -> TSFunctionDecl? { + return try impl.encodeSignature() + } + + func encodeDecl() throws -> TSFunctionDecl? { + return try impl.encodeDecl() + } + } diff --git a/Sources/CodableToTypeScript/TypeConverter/GenericParamConverter.swift b/Sources/CodableToTypeScript/TypeConverter/GenericParamConverter.swift index a18b9fe..3d553af 100644 --- a/Sources/CodableToTypeScript/TypeConverter/GenericParamConverter.swift +++ b/Sources/CodableToTypeScript/TypeConverter/GenericParamConverter.swift @@ -8,4 +8,8 @@ struct GenericParamConverter: TypeConverter { func hasDecode() throws -> Bool { return true } + + func hasEncode() throws -> Bool { + return true + } } diff --git a/Sources/CodableToTypeScript/TypeConverter/OptionalConverter.swift b/Sources/CodableToTypeScript/TypeConverter/OptionalConverter.swift index 9133b7d..1ab107b 100644 --- a/Sources/CodableToTypeScript/TypeConverter/OptionalConverter.swift +++ b/Sources/CodableToTypeScript/TypeConverter/OptionalConverter.swift @@ -29,14 +29,11 @@ struct OptionalConverter: TypeConverter { } func decodeName() throws -> String? { - return generator.helperLibrary().name(.optionalDecodeFunction) + return generator.helperLibrary().name(.optionalDecode) } func callDecode(json: any TSExpr) throws -> any TSExpr { - guard try hasDecode() else { return json } - let decodeName = try decodeName().unwrap(name: "decode name") - return try generator.callDecode( - callee: TSIdentExpr(decodeName), + return try `default`.callDecode( genericArgs: [try wrapped(limit: nil).type], json: json ) @@ -44,11 +41,37 @@ struct OptionalConverter: TypeConverter { func callDecodeField(json: any TSExpr) throws -> any TSExpr { guard try hasDecode() else { return json } - let decodeName = generator.helperLibrary().name(.optionalFieldDecodeFunction) + let decodeName = generator.helperLibrary().name(.optionalFieldDecode) return try generator.callDecode( callee: TSIdentExpr(decodeName), genericArgs: [try wrapped(limit: 1).type], json: json ) } + + func hasEncode() throws -> Bool { + return try wrapped(limit: nil).hasEncode() + } + + func encodeName() throws -> String { + return generator.helperLibrary().name(.optionalEncode) + } + + func callEncode(entity: any TSExpr) throws -> any TSExpr { + return try `default`.callEncode( + genericArgs: [try wrapped(limit: nil).type], + entity: entity + ) + } + + func callEncodeField(entity: any TSExpr) throws -> any TSExpr { + guard try hasEncode() else { return entity } + let encodeName = generator.helperLibrary().name(.optionalFieldEncode) + return try generator.callEncode( + callee: TSIdentExpr(encodeName), + genericArgs: [try wrapped(limit: 1).type], + entity: entity + ) + } + } diff --git a/Sources/CodableToTypeScript/TypeConverter/StructConverter.swift b/Sources/CodableToTypeScript/TypeConverter/StructConverter.swift index 951e90d..a1410e9 100644 --- a/Sources/CodableToTypeScript/TypeConverter/StructConverter.swift +++ b/Sources/CodableToTypeScript/TypeConverter/StructConverter.swift @@ -71,4 +71,43 @@ struct StructConverter: TypeConverter { return decl } + + func hasEncode() throws -> Bool { + for field in `struct`.decl.storedProperties { + if try generator.converter(for: field.interfaceType).hasEncode() { + return true + } + } + return false + } + + func encodeDecl() throws -> TSFunctionDecl? { + guard let decl = try encodeSignature() else { return nil } + + var fields: [TSObjectExpr.Field] = [] + + for field in `struct`.decl.storedProperties { + var expr: any TSExpr = TSMemberExpr( + base: TSIdentExpr("entity"), + name: TSIdentExpr(field.name) + ) + + expr = try generator.converter(for: field.interfaceType) + .callEncodeField(entity: expr) + + fields.append( + .init( + name: field.name, + value: expr + ) + ) + } + + decl.body.elements.append( + TSReturnStmt(TSObjectExpr(fields)) + ) + + return decl + } + } diff --git a/Sources/CodableToTypeScript/TypeConverter/TypeConverter.swift b/Sources/CodableToTypeScript/TypeConverter/TypeConverter.swift index 9c3623a..6281995 100644 --- a/Sources/CodableToTypeScript/TypeConverter/TypeConverter.swift +++ b/Sources/CodableToTypeScript/TypeConverter/TypeConverter.swift @@ -16,6 +16,13 @@ public protocol TypeConverter { func callDecodeField(json: any TSExpr) throws -> any TSExpr func decodeSignature() throws -> TSFunctionDecl? func decodeDecl() throws -> TSFunctionDecl? + func hasEncode() throws -> Bool + func encodeName() throws -> String + func boundEncode() throws -> any TSExpr + func callEncode(entity: any TSExpr) throws -> any TSExpr + func callEncodeField(entity: any TSExpr) throws -> any TSExpr + func encodeSignature() throws -> TSFunctionDecl? + func encodeDecl() throws -> TSFunctionDecl? func ownDecls() throws -> TypeOwnDeclarations func source() throws -> TSSourceFile } @@ -70,6 +77,30 @@ extension TypeConverter { return try `default`.decodeDecl() } + public func encodeName() throws -> String { + return try `default`.encodeName() + } + + public func boundEncode() throws -> any TSExpr { + return try `default`.boundEncode() + } + + public func callEncode(entity: any TSExpr) throws -> any TSExpr { + return try `default`.callEncode(entity: entity) + } + + public func callEncodeField(entity: any TSExpr) throws -> any TSExpr { + return try `default`.callEncodeField(entity: entity) + } + + public func encodeSignature() throws -> TSFunctionDecl? { + return try `default`.encodeSignature() + } + + public func encodeDecl() throws -> TSFunctionDecl? { + return try `default`.encodeDecl() + } + // MARK: - extensions public func genericArgs() throws -> [any TypeConverter] { return try type.genericArgs.map { (type) in @@ -92,7 +123,8 @@ extension TypeConverter { return TypeOwnDeclarations( entityType: try typeDecl(for: .entity).unwrap(name: "entity type decl"), jsonType: try typeDecl(for: .json), - decodeFunction: try decodeDecl() + decodeFunction: try decodeDecl(), + encodeFunction: try encodeDecl() ) } diff --git a/Sources/CodableToTypeScript/TypeConverter/TypeMapConverter.swift b/Sources/CodableToTypeScript/TypeConverter/TypeMapConverter.swift index 5a88a1f..d291195 100644 --- a/Sources/CodableToTypeScript/TypeConverter/TypeMapConverter.swift +++ b/Sources/CodableToTypeScript/TypeConverter/TypeMapConverter.swift @@ -35,4 +35,15 @@ public struct TypeMapConverter: TypeConverter { public func decodeName() throws -> String { return try entry.decode.unwrap(name: "entry.decode") } + + public func hasEncode() throws -> Bool { + if let _ = entry.encode { + return true + } + return false + } + + public func encodeName() throws -> String { + return try entry.encode.unwrap(name: "entry.encode") + } } diff --git a/Sources/CodableToTypeScript/Value/TypeMap.swift b/Sources/CodableToTypeScript/Value/TypeMap.swift index bbc7834..dddf9d1 100644 --- a/Sources/CodableToTypeScript/Value/TypeMap.swift +++ b/Sources/CodableToTypeScript/Value/TypeMap.swift @@ -4,14 +4,17 @@ public struct TypeMap { public struct Entry { public init( name: String, - decode: String? = nil + decode: String? = nil, + encode: String? = nil ) { self.name = name self.decode = decode + self.encode = encode } public var name: String public var decode: String? + public var encode: String? } public typealias MapFunction = (any SType) -> Entry? diff --git a/Sources/CodableToTypeScript/Value/TypeOwnDeclarations.swift b/Sources/CodableToTypeScript/Value/TypeOwnDeclarations.swift index 4b24520..1fa6fa3 100644 --- a/Sources/CodableToTypeScript/Value/TypeOwnDeclarations.swift +++ b/Sources/CodableToTypeScript/Value/TypeOwnDeclarations.swift @@ -4,15 +4,18 @@ public struct TypeOwnDeclarations { public var entityType: TSTypeDecl public var jsonType: TSTypeDecl? public var decodeFunction: TSFunctionDecl? + public var encodeFunction: TSFunctionDecl? public init( entityType: TSTypeDecl, jsonType: TSTypeDecl?, - decodeFunction: TSFunctionDecl? + decodeFunction: TSFunctionDecl?, + encodeFunction: TSFunctionDecl? ) { self.entityType = entityType self.jsonType = jsonType self.decodeFunction = decodeFunction + self.encodeFunction = encodeFunction } public var decls: [any TSDecl] { @@ -28,6 +31,10 @@ public struct TypeOwnDeclarations { decls.append(decl) } + if let decl = encodeFunction { + decls.append(decl) + } + return decls } } diff --git a/Tests/CodableToTypeScriptTests/Generate/GenerateCustomTypeTests.swift b/Tests/CodableToTypeScriptTests/Generate/GenerateCustomTypeTests.swift index 9ffc5b8..fba3710 100644 --- a/Tests/CodableToTypeScriptTests/Generate/GenerateCustomTypeTests.swift +++ b/Tests/CodableToTypeScriptTests/Generate/GenerateCustomTypeTests.swift @@ -161,6 +161,14 @@ export type S = { func decodeName() throws -> String { return "Date_decode" } + + func hasEncode() throws -> Bool { + return true + } + + func encodeName() throws -> String { + return "Date_encode" + } } func testCustomTypeConverter() throws { diff --git a/Tests/CodableToTypeScriptTests/Generate/GenerateEncodeTests.swift b/Tests/CodableToTypeScriptTests/Generate/GenerateEncodeTests.swift new file mode 100644 index 0000000..483ec98 --- /dev/null +++ b/Tests/CodableToTypeScriptTests/Generate/GenerateEncodeTests.swift @@ -0,0 +1,104 @@ +import XCTest +import CodableToTypeScript + +final class GenerateEncodeTests: GenerateTestCaseBase { + func dateTypeMap() -> TypeMap { + var typeMap = TypeMap() + typeMap.table["Date"] = .init(name: "Date", decode: "Date_decode", encode: "Date_encode") + return typeMap + } + + func testEnum() throws { + try assertGenerate( + source: """ +enum E { + case a + case b(Date) +} +""", + typeMap: dateTypeMap(), + expecteds: [""" +export type E = { + kind: "a"; + a: {}; +} | { + kind: "b"; + b: { + _0: Date; + }; +}; +""", """ +export type E_JSON = { + a: {}; +} | { + b: { + _0: Date_JSON; + }; +}; +""", """ +export function E_encode(entity: E): E_JSON { + switch (entity.kind) { + case "a": + { + return { + a: {} + }; + } + case "b": + { + const e = entity.b; + return { + b: { + _0: Date_encode(e._0) + } + }; + } + default: + const check: never = entity; + throw new Error("invalid case: " + check); + } +} +"""] + ) + } + + func testStruct() throws { + try assertGenerate( + source: """ +struct S { + var a: Date + var b: Date? + var c: Date?? + var d: [Date] +} +""", + typeMap: dateTypeMap(), + expecteds: [""" +export type S = { + a: Date; + b?: Date; + c?: Date | null; + d: Date[]; +}; +""", """ +export type S_JSON = { + a: Date_JSON; + b?: Date_JSON; + c?: Date_JSON | null; + d: Date_JSON[]; +}; +""", """ +export function S_encode(entity: S): S_JSON { + return { + a: Date_encode(entity.a), + b: OptionalField_encode(entity.b, Date_encode), + c: OptionalField_encode(entity.c, (entity: Date | null): Date_JSON | null => { + return Optional_encode(entity, Date_encode); + }), + d: Array_encode(entity.d, Date_encode) + }; +} +"""] + ) + } +} diff --git a/Tests/CodableToTypeScriptTests/HelperLibraryTests.swift b/Tests/CodableToTypeScriptTests/HelperLibraryTests.swift index 21c5c48..bcc1cbe 100644 --- a/Tests/CodableToTypeScriptTests/HelperLibraryTests.swift +++ b/Tests/CodableToTypeScriptTests/HelperLibraryTests.swift @@ -9,24 +9,42 @@ final class HelperLibraryTests: XCTestCase { let actual = code.print() +// print(actual) + XCTAssertTrue(actual.contains(""" export function identity(json: T): T """)) XCTAssertTrue(actual.contains(""" -export function OptionalField_decode(json: T | undefined, T_decode: (json: T) => U): U | undefined +export function OptionalField_decode(json: T_JSON | undefined, T_decode: (json: T_JSON) => T): T | undefined +""")) + + XCTAssertTrue(actual.contains(""" +export function OptionalField_encode(entity: T | undefined, T_encode: (entity: T) => T_JSON): T_JSON | undefined +""")) + + XCTAssertTrue(actual.contains(""" +export function Optional_decode(json: T_JSON | null, T_decode: (json: T_JSON) => T): T | null +""")) + + XCTAssertTrue(actual.contains(""" +export function Optional_encode(entity: T | null, T_encode: (entity: T) => T_JSON): T_JSON | null +""")) + + XCTAssertTrue(actual.contains(""" +export function Array_decode(json: T_JSON[], T_decode: (json: T_JSON) => T): T[] """)) XCTAssertTrue(actual.contains(""" -export function Optional_decode(json: T | null, T_decode: (json: T) => U): U | null +export function Array_encode(entity: T[], T_encode: (entity: T) => T_JSON): T_JSON[] """)) XCTAssertTrue(actual.contains(""" -export function Array_decode(json: T[], T_decode: (json: T) => U): U[] +export function Dictionary_decode(json: { [key: string]: T_JSON; }, T_decode: (json: T_JSON) => T): { [key: string]: T; } """)) XCTAssertTrue(actual.contains(""" -export function Dictionary_decode(json: { [key: string]: T; }, T_decode: (json: T) => U): { [key: string]: U; } +export function Dictionary_encode(entity: { [key: string]: T; }, T_encode: (entity: T) => T_JSON): { [key: string]: T_JSON; } """)) } }