Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.2.0")
.package(url: "https://github.com/omochi/TypeScriptAST", from: "1.3.0")
],
targets: [
.target(
Expand Down
34 changes: 20 additions & 14 deletions Sources/CodableToTypeScript/Extensions/STypeEx.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import SwiftTypeReader

extension TypeDecl {
func namePath() -> NamePath {
internal func namePath() -> NamePath {
return declaredInterfaceType.namePath()
}

func walk(_ body: (any TypeDecl) throws -> Void) rethrows {
try body(self)
public func walk(_ body: (any TypeDecl) throws -> Bool) rethrows {
guard try body(self) else { return }

guard let nominal = asNominalType else { return }

Expand All @@ -17,28 +17,34 @@ extension TypeDecl {
}

extension NominalTypeDecl {
func isStandardLibraryType(_ name: String) -> Bool {
public func isStandardLibraryType(_ name: String) -> Bool {
return moduleContext.name == "Swift" &&
self.name == name
}

func hasStringRawValue() -> Bool {
return inheritedTypes.contains { (type) in
type.isStandardLibraryType("String")
public func isRawRepresentable() -> (any SType)? {
for type in inheritedTypes {
if type.isStandardLibraryType("String") { return type }
}

if let property = find(name: "rawValue") as? VarDecl {
return property.interfaceType
}

return nil
}
}

extension SType {
var typeDecl: (any TypeDecl)? {
internal var typeDecl: (any TypeDecl)? {
switch self {
case let type as any NominalType: return type.nominalTypeDecl
case let param as GenericParamType: return param.decl
default: return nil
}
}

var genericArgs: [any SType] {
internal var genericArgs: [any SType] {
if let self = self.asNominal {
return self.genericArgs
} else if let self = self.asError {
Expand All @@ -54,7 +60,7 @@ extension SType {
}
}

func namePath() -> NamePath {
internal func namePath() -> NamePath {
let repr = toTypeRepr(containsModule: false)

if let ident = repr.asIdent {
Expand All @@ -66,7 +72,7 @@ extension SType {
}
}

func unwrapOptional(limit: Int?) -> (wrapped: any SType, depth: Int)? {
internal func unwrapOptional(limit: Int?) -> (wrapped: any SType, depth: Int)? {
var type: any SType = self
var depth = 0
while type.isStandardLibraryType("Optional"),
Expand All @@ -87,21 +93,21 @@ extension SType {
return (wrapped: type, depth: depth)
}

func asArray() -> (array: StructType, element: any SType)? {
internal func asArray() -> (array: StructType, element: any SType)? {
guard isStandardLibraryType("Array"),
let array = self.asStruct,
let element = array.genericArgs[safe: 0] else { return nil }
return (array: array, element: element)
}

func asDictionary() -> (dictionary: StructType, value: any SType)? {
internal func asDictionary() -> (dictionary: StructType, value: any SType)? {
guard isStandardLibraryType("Dictionary"),
let dict = self.asStruct,
let value = dict.genericArgs[safe: 1] else { return nil }
return (dictionary: dict, value: value)
}

func isStandardLibraryType(_ name: String) -> Bool {
public func isStandardLibraryType(_ name: String) -> Bool {
guard let self = self.asNominal else { return false }
return self.nominalTypeDecl.isStandardLibraryType(name)
}
Expand Down
4 changes: 0 additions & 4 deletions Sources/CodableToTypeScript/Generator/CodeGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ public final class CodeGenerator {
)
}

public func converter(for decl: any TypeDecl) throws -> any TypeConverter {
return try converter(for: decl.declaredInterfaceType)
}

private func implConverter(for type: any SType) throws -> any TypeConverter {
return try typeConverterProvider.provide(generator: self, type: type)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ public struct DefaultTypeConverter {
return (type: type, isOptional: false)
}

public func phantomType(for target: GenerationTarget, name: String) throws -> any TSType {
let body = try self.converter().type(for: target)
let tag = TSObjectType([
.init(name: name, type: TSIdentType.never)
])
return TSIntersectionType([body, tag])
}

public func decodeName() throws -> String {
let converter = try self.converter()
guard try converter.hasDecode() else {
Expand Down Expand Up @@ -111,7 +119,11 @@ public struct DefaultTypeConverter {
public func callDecode(genericArgs: [any SType], json: any TSExpr) throws -> any TSExpr {
let converter = try self.converter()
guard try converter.hasDecode() else {
return json
var expr = json
if try converter.hasJSONType() {
expr = TSAsExpr(expr, try converter.type(for: .entity))
}
return expr
}
let decodeName = try converter.decodeName()
return try generator.callDecode(
Expand Down Expand Up @@ -241,7 +253,11 @@ public struct DefaultTypeConverter {
public func callEncode(genericArgs: [any SType], entity: any TSExpr) throws -> any TSExpr {
let converter = try self.converter()
guard try converter.hasEncode() else {
return entity
var expr = entity
if try converter.hasJSONType() {
expr = TSAsExpr(expr, try converter.type(for: .json))
}
return expr
}
let encodeName = try converter.encodeName()
return try generator.callEncode(
Expand Down
52 changes: 40 additions & 12 deletions Sources/CodableToTypeScript/TypeConverter/EnumConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,40 @@ import SwiftTypeReader
import TypeScriptAST

struct EnumConverter: TypeConverter {
init(generator: CodeGenerator, `enum`: EnumType) {
self.generator = generator
self.`enum` = `enum`

let decl = `enum`.decl

if decl.caseElements.isEmpty {
self.kind = .never
return
}

if let raw = decl.isRawRepresentable() {
if raw.isStandardLibraryType("String") {
self.kind = .string
return
}
}

self.kind = .normal
}

var generator: CodeGenerator
var `enum`: EnumType

var swiftType: any SType { `enum` }

private var decl: EnumDecl { `enum`.decl }
private var kind: Kind

enum Kind {
case never
case string
case normal
}

func typeDecl(for target: GenerationTarget) throws -> TSTypeDecl? {
switch target {
Expand All @@ -18,14 +46,15 @@ struct EnumConverter: TypeConverter {

let genericParams = try self.genericParams().map { try $0.name(for: target) }

if decl.caseElements.isEmpty {
switch kind {
case .never:
return TSTypeDecl(
modifiers: [.export],
name: try name(for: target),
genericParams: genericParams,
type: TSIdentType.never
)
} else if decl.hasStringRawValue() {
case .string:
let items: [any TSType] = decl.caseElements.map { (ce) in
TSStringLiteralType(ce.name)
}
Expand All @@ -36,6 +65,7 @@ struct EnumConverter: TypeConverter {
genericParams: genericParams,
type: TSUnionType(items)
)
default: break
}

let items: [any TSType] = try decl.caseElements.map { (ce) in
Expand Down Expand Up @@ -89,13 +119,11 @@ struct EnumConverter: TypeConverter {
}

func hasDecode() throws -> Bool {
if decl.caseElements.isEmpty {
return false
} else if decl.hasStringRawValue() {
return false
switch kind {
case .never: return false
case .string: return false
case .normal: return true
}

return true
}

func decodeDecl() throws -> TSFunctionDecl? {
Expand All @@ -107,10 +135,10 @@ struct EnumConverter: TypeConverter {
}

func hasEncode() throws -> Bool {
if decl.caseElements.isEmpty {
return false
} else if decl.hasStringRawValue() {
return false
switch kind {
case .never: return false
case .string: return false
case .normal: break
}

for caseElement in decl.caseElements {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ struct GeneratorProxyConverter: TypeConverter {
return try impl.fieldType(for: target)
}

func phantomType(for target: GenerationTarget, name: String) throws -> TSType {
return try impl.phantomType(for: target, name: name)
}

func typeDecl(for target: GenerationTarget) throws -> TSTypeDecl? {
return try impl.typeDecl(for: target)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ struct OptionalConverter: TypeConverter {
)
}

func phantomType(for target: GenerationTarget, name: String) throws -> any TSType {
let wrapped = try self.wrapped(limit: nil)
return TSUnionType([
try wrapped.phantomType(for: target, name: name),
TSIdentType.null
])
}

func typeDecl(for target: GenerationTarget) throws -> TSTypeDecl? {
throw MessageError("Unsupported type: \(swiftType)")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import SwiftTypeReader
import TypeScriptAST

struct RawRepresentableConverter: TypeConverter {
var generator: CodeGenerator
var swiftType: any SType
var rawValueType: any TypeConverter

func typeDecl(for target: GenerationTarget) throws -> TSTypeDecl? {
switch target {
case .entity: break
case .json:
guard try rawValueType.hasJSONType() else { return nil }
}

let name = try self.name(for: target)
let type = try rawValueType.phantomType(for: target, name: name)

return TSTypeDecl(
modifiers: [.export],
name: name,
genericParams: try genericParams().map { try $0.name(for: target) },
type: type
)
}

func hasDecode() throws -> Bool {
return try rawValueType.hasJSONType()
}

func decodeDecl() throws -> TSFunctionDecl? {
guard let decl = try decodeSignature() else { return nil }

var expr = try rawValueType.callDecode(json: TSIdentExpr("json"))
expr = TSAsExpr(expr, try self.type(for: .entity))
decl.body.elements.append(
TSReturnStmt(expr)
)

return decl
}

func hasEncode() throws -> Bool {
return try rawValueType.hasJSONType()
}

func encodeDecl() throws -> TSFunctionDecl? {
guard let decl = try encodeSignature() else { return nil }

var expr = try rawValueType.callEncode(entity: TSIdentExpr("entity"))
expr = TSAsExpr(expr, try self.type(for: .json))
decl.body.elements.append(
TSReturnStmt(expr)
)

return decl
}
}
10 changes: 6 additions & 4 deletions Sources/CodableToTypeScript/TypeConverter/TypeConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public protocol TypeConverter {
func hasJSONType() throws -> Bool
func type(for target: GenerationTarget) throws -> any TSType
func fieldType(for target: GenerationTarget) throws -> (type: any TSType, isOptional: Bool)
func phantomType(for target: GenerationTarget, name: String) throws -> any TSType
func typeDecl(for target: GenerationTarget) throws -> TSTypeDecl?
func hasDecode() throws -> Bool
func decodeName() throws -> String
Expand Down Expand Up @@ -49,9 +50,9 @@ extension TypeConverter {
return try `default`.fieldType(for: target)
}

// public func typeDecl(for target: GenerationTarget) throws -> TSTypeDecl? {
// return try `default`.typeDecl(for: target)
// }
public func phantomType(for target: GenerationTarget, name: String) throws -> any TSType {
return try `default`.phantomType(for: target, name: name)
}

public func decodeName() throws -> String {
return try `default`.decodeName()
Expand Down Expand Up @@ -107,7 +108,7 @@ extension TypeConverter {
return []
}
return try genericContext.genericParams.items.map { (param) in
try generator.converter(for: param)
try generator.converter(for: param.declaredInterfaceType)
}
}

Expand All @@ -127,6 +128,7 @@ extension TypeConverter {
try typeDecl.walk { (type) in
let converter = try generator.converter(for: type.declaredInterfaceType)
decls += try converter.ownDecls().decls
return true
}
}

Expand Down
Loading