gotlin/internal/lang/mapping.go

269 lines
9.7 KiB
Go

package lang
import (
"fmt"
"strconv"
"strings"
)
type mappingPair struct{ source, target, function string }
type mappingState struct {
functions map[string]string
pairs []mappingPair
}
func (g *goGenerator) mappingTopLevelTarget(target string) string {
if _, ok := g.classForType(target); ok && !strings.HasPrefix(target, "*") {
return "*" + target
}
return target
}
func (g *goGenerator) ensureMapping(source, target, path string) (string, error) {
key := source + "->" + target
if function, ok := g.mappings.functions[key]; ok {
return function, nil
}
if err := g.validateMapping(source, target, path, map[string]bool{}); err != nil {
return "", err
}
function := fmt.Sprintf("gotlinMap%d", len(g.mappings.pairs)+1)
g.mappings.functions[key] = function
g.mappings.pairs = append(g.mappings.pairs, mappingPair{source: source, target: target, function: function})
return function, nil
}
func (g *goGenerator) validateMapping(source, target, path string, seen map[string]bool) error {
if source == target {
return nil
}
key := source + "->" + target
if seen[key] {
return nil
}
seen[key] = true
if strings.HasSuffix(source, "?") || strings.HasSuffix(target, "?") {
if strings.HasSuffix(source, "?") && !strings.HasSuffix(target, "?") {
return mappingError(path, source, target)
}
return g.validateMapping(strings.TrimSuffix(source, "?"), strings.TrimSuffix(target, "?"), path, seen)
}
if sourceBase, sourceArgs, ok := parseGenericType(source); ok {
targetBase, targetArgs, targetOK := parseGenericType(target)
if !targetOK || sourceBase != targetBase || len(sourceArgs) != len(targetArgs) {
return mappingError(path, source, target)
}
for i := range sourceArgs {
if sourceBase == "Map" || sourceBase == "MutableMap" {
if i == 0 && sourceArgs[i] != targetArgs[i] {
return mappingError(path+".<key>", sourceArgs[i], targetArgs[i])
}
}
if err := g.validateMapping(sourceArgs[i], targetArgs[i], path+"[]", seen); err != nil {
return err
}
}
return nil
}
sourceClass, sourceClassOK := g.classForType(source)
targetClass, targetClassOK := g.classForType(target)
if sourceClassOK || targetClassOK {
if !sourceClassOK || !targetClassOK {
return mappingError(path, source, target)
}
for _, targetField := range targetClass.Fields {
sourceField, ok := classFieldByName(sourceClass, targetField.Name)
fieldPath := path + "." + targetField.Name
if !ok {
return fmt.Errorf("cannot map %s: source field is missing for %s.%s", fieldPath, targetClass.Name, targetField.Name)
}
if err := g.validateMapping(sourceField.Type, targetField.Type, fieldPath, seen); err != nil {
return err
}
}
return nil
}
sourceEnum, sourceEnumOK := g.enums[strings.TrimPrefix(source, "*")]
targetEnum, targetEnumOK := g.enums[strings.TrimPrefix(target, "*")]
if sourceEnumOK || targetEnumOK {
if sourceEnumOK && enumIsString(sourceEnum) && target == "String" {
return nil
}
if targetEnumOK && enumIsString(targetEnum) && source == "String" {
return nil
}
if !sourceEnumOK || !targetEnumOK {
return mappingError(path, source, target)
}
for _, sourceVariant := range sourceEnum.Variants {
targetVariant := enumVariant(targetEnum, sourceVariant.Name)
variantPath := path + "." + sourceVariant.Name
if targetVariant == nil {
return fmt.Errorf("cannot map %s: target enum %s has no compatible variant", variantPath, targetEnum.Name)
}
if len(sourceVariant.PayloadTypes) != len(targetVariant.PayloadTypes) {
return fmt.Errorf("cannot map %s: payload count %d is incompatible with %d", variantPath, len(sourceVariant.PayloadTypes), len(targetVariant.PayloadTypes))
}
for i := range sourceVariant.PayloadTypes {
if err := g.validateMapping(sourceVariant.PayloadTypes[i], targetVariant.PayloadTypes[i], fmt.Sprintf("%s[%d]", variantPath, i), seen); err != nil {
return err
}
}
}
return nil
}
return mappingError(path, source, target)
}
func mappingError(path, source, target string) error {
return fmt.Errorf("cannot map %s: %s is incompatible with %s", path, source, target)
}
func classFieldByName(class ClassDecl, name string) (FieldDecl, bool) {
for _, field := range class.Fields {
if field.Name == name {
return field, true
}
}
return FieldDecl{}, false
}
func mappingFieldName(class ClassDecl, field FieldDecl) string {
if class.Data && !field.Private {
return exportedGoName(field.Name)
}
return field.Name
}
func (g *goGenerator) emitMapping(pair mappingPair) error {
g.line(fmt.Sprintf("func %s(source %s) %s {", pair.function, mapGoType(pair.source), mapGoType(pair.target)))
g.indentLevel++
if sourceEnum, ok := g.enums[strings.TrimPrefix(pair.source, "*")]; ok {
if enumIsString(sourceEnum) && pair.target == "String" {
g.line("return string(source)")
g.indentLevel--
g.line("}")
return nil
}
targetEnum := g.enums[strings.TrimPrefix(pair.target, "*")]
if enumIsString(sourceEnum) && enumIsString(targetEnum) {
g.line("return " + targetEnum.Name + "(source)")
g.indentLevel--
g.line("}")
return nil
}
g.line("switch value := source.(type) {")
g.indentLevel++
for _, sourceVariant := range sourceEnum.Variants {
targetVariant := enumVariant(targetEnum, sourceVariant.Name)
g.line("case *" + sourceEnum.Name + sourceVariant.Name + ":")
g.indentLevel++
fields := make([]string, 0, len(sourceVariant.PayloadTypes))
for i := range sourceVariant.PayloadTypes {
expr, err := g.mappingExpr(fmt.Sprintf("value.Value%d", i), sourceVariant.PayloadTypes[i], targetVariant.PayloadTypes[i], sourceEnum.Name+"."+sourceVariant.Name)
if err != nil {
return err
}
fields = append(fields, fmt.Sprintf("Value%d: %s", i, expr))
}
g.line("return &" + targetEnum.Name + targetVariant.Name + "{" + strings.Join(fields, ", ") + "}")
g.indentLevel--
}
g.indentLevel--
g.line("}")
g.line(`panic("unreachable enum mapping")`)
} else if targetEnum, ok := g.enums[strings.TrimPrefix(pair.target, "*")]; ok && pair.source == "String" && enumIsString(targetEnum) {
g.line("switch source {")
g.indentLevel++
for _, variant := range targetEnum.Variants {
g.line("case " + strconv.Quote(enumStringValue(variant)) + ":")
g.indentLevel++
g.line("return " + targetEnum.Name + variant.Name)
g.indentLevel--
}
g.indentLevel--
g.line("}")
g.line(`panic("unknown enum string: " + source)`)
} else {
expr, err := g.mappingExpr("source", pair.source, pair.target, strings.TrimPrefix(pair.source, "*"))
if err != nil {
return err
}
g.line("return " + expr)
}
g.indentLevel--
g.line("}")
return nil
}
func (g *goGenerator) mappingExpr(expr, source, target, path string) (string, error) {
if source == target {
return expr, nil
}
if strings.HasSuffix(source, "?") || strings.HasSuffix(target, "?") {
sourceInner := strings.TrimSuffix(source, "?")
targetInner := strings.TrimSuffix(target, "?")
inner, err := g.mappingExpr("*value", sourceInner, targetInner, path)
if err != nil {
return "", err
}
if strings.HasSuffix(source, "?") {
return fmt.Sprintf("func(value %s) %s { if value == nil { return nil }; mapped := %s; return &mapped }(%s)", mapGoType(source), mapGoType(target), inner, expr), nil
}
inner, err = g.mappingExpr("value", sourceInner, targetInner, path)
if err != nil {
return "", err
}
return fmt.Sprintf("func(value %s) %s { mapped := %s; return &mapped }(%s)", mapGoType(source), mapGoType(target), inner, expr), nil
}
if sourceBase, sourceArgs, ok := parseGenericType(source); ok {
_, targetArgs, _ := parseGenericType(target)
if sourceBase == "List" || sourceBase == "MutableList" {
item, err := g.mappingExpr("item", sourceArgs[0], targetArgs[0], path+"[]")
if err != nil {
return "", err
}
return fmt.Sprintf("func(values %s) %s { var result %s; for _, item := range values { result = append(result, %s) }; return result }(%s)", mapGoType(source), mapGoType(target), mapGoType(target), item, expr), nil
}
if sourceBase == "Map" || sourceBase == "MutableMap" {
value, err := g.mappingExpr("item", sourceArgs[1], targetArgs[1], path+"[]")
if err != nil {
return "", err
}
return fmt.Sprintf("func(values %s) %s { result := make(%s, len(values)); for key, item := range values { result[key] = %s }; return result }(%s)", mapGoType(source), mapGoType(target), mapGoType(target), value, expr), nil
}
}
if sourceClass, ok := g.classForType(source); ok {
targetClass, _ := g.classForType(target)
fields := make([]string, 0, len(targetClass.Fields))
for _, targetField := range targetClass.Fields {
sourceField, _ := classFieldByName(sourceClass, targetField.Name)
mapped, err := g.mappingExpr(expr+"."+mappingFieldName(sourceClass, sourceField), sourceField.Type, targetField.Type, path+"."+targetField.Name)
if err != nil {
return "", err
}
fields = append(fields, mappingFieldName(targetClass, targetField)+": "+mapped)
}
literal := targetClass.Name + "{" + strings.Join(fields, ", ") + "}"
if strings.HasPrefix(target, "*") {
literal = "&" + literal
}
if strings.HasPrefix(source, "*") {
return fmt.Sprintf("func(value %s) %s { if value == nil { return nil }; return %s }(%s)", mapGoType(source), mapGoType(target), strings.ReplaceAll(literal, expr+".", "value."), expr), nil
}
return literal, nil
}
if _, ok := g.enums[strings.TrimPrefix(source, "*")]; ok {
function, err := g.ensureMapping(source, target, path)
if err != nil {
return "", err
}
return function + "(" + expr + ")", nil
}
if targetEnum, ok := g.enums[strings.TrimPrefix(target, "*")]; ok && source == "String" && enumIsString(targetEnum) {
function, err := g.ensureMapping(source, target, path)
if err != nil {
return "", err
}
return function + "(" + expr + ")", nil
}
return "", mappingError(path, source, target)
}