pkg/clientgen/client.go
Rama main · réplica de lectura del repositorio original.
// Package clientgen generates code for use with Encore apps.
package clientgen
import (
"bytes"
"errors"
"fmt"
"path/filepath"
"strings"
"encr.dev/pkg/clientgen/clientgentypes"
"encr.dev/pkg/clientgen/openapi"
"encr.dev/pkg/errinsrc/srcerrors"
meta "encr.dev/proto/encore/parser/meta/v1"
)
// Lang represents a programming language or dialect that we support generating code for.
type Lang string
// These constants represent supported languages.
const (
LangUnknown Lang = ""
LangTypeScript Lang = "typescript"
LangJavascript Lang = "javascript"
LangGo Lang = "go"
LangOpenAPI Lang = "openapi"
)
type generator interface {
Generate(p clientgentypes.GenerateParams) error
Version() int // The version of the generator.
}
// ErrUnknownLang is reported by Generate when the language is not known.
var ErrUnknownLang = errors.New("unknown language")
// quoteJSString renders s as a double-quoted JavaScript/TypeScript string
// literal, escaping every character that could otherwise break out of the
// literal and inject code into the generated client. Both attacker-influenced
// values (e.g. header/query wire names) and ordinary identifiers flow through
// here, so it must produce valid JS for arbitrary input.
func quoteJSString(s string) string {
var b strings.Builder
b.Grow(len(s) + 2)
b.WriteByte('"')
for _, r := range s {
switch r {
case '\\':
b.WriteString(`\\`)
case '"':
b.WriteString(`\"`)
case '\n':
b.WriteString(`\n`)
case '\r':
b.WriteString(`\r`)
case '\t':
b.WriteString(`\t`)
case '\u2028':
// Line separator: valid in JS string literals only since ES2019.
b.WriteString(`\u2028`)
case '\u2029':
// Paragraph separator: valid in JS string literals only since ES2019.
b.WriteString(`\u2029`)
default:
if r < 0x20 {
fmt.Fprintf(&b, `\u%04x`, r)
} else {
b.WriteRune(r)
}
}
}
b.WriteByte('"')
return b.String()
}
// escapeDocComment neutralizes any "*/" sequence in a line destined for a
// generated /* ... */ block comment so it cannot terminate the comment early
// and turn the following text into executable code.
func escapeDocComment(line string) string {
return strings.ReplaceAll(line, "*/", "*\\/")
}
// Detect attempts to detect the language from the given filename.
func Detect(path string) (lang Lang, ok bool) {
suffix := strings.ToLower(filepath.Ext(path))
switch suffix {
case ".ts":
return LangTypeScript, true
case ".js":
return LangJavascript, true
case ".go":
return LangGo, true
default:
return LangUnknown, false
}
}
// Client generates an API client based on the given app metadata.
// ServiceNames are the services to include in the output.
// If it's nil, all services are included.
func Client(
lang Lang,
appSlug string,
md *meta.Data,
services clientgentypes.ServiceSet,
tags clientgentypes.TagSet,
opts clientgentypes.Options,
) (code []byte, err error) {
defer func() {
if e := recover(); e != nil {
err = srcerrors.UnhandledPanic(e)
}
}()
var gen generator
switch lang {
case LangTypeScript:
if opts.TSSharedTypes && md.Language == meta.Lang_TYPESCRIPT {
gen = &typescript{generatorVersion: typescriptGenLatestVersion, sharedTypes: true, clientTarget: opts.TSClientTarget}
} else {
gen = &typescript{generatorVersion: typescriptGenLatestVersion, sharedTypes: false}
}
case LangJavascript:
gen = &javascript{generatorVersion: javascriptGenLatestVersion}
case LangGo:
gen = &golang{generatorVersion: goGenLatestVersion}
case LangOpenAPI:
gen = openapi.New(openapi.LatestVersion)
default:
return nil, ErrUnknownLang
}
var buf bytes.Buffer
params := clientgentypes.GenerateParams{
Buf: &buf,
AppSlug: appSlug,
Meta: md,
Services: services,
Tags: tags,
Options: opts,
}
if err := gen.Generate(params); err != nil {
return nil, fmt.Errorf("genclient.Generate %s %s: %v", lang, appSlug, err)
}
return buf.Bytes(), nil
}
// getServiceDoc returns the documentation for a service by looking up the
// root package matching the service's rel_path
func getServiceDoc(md *meta.Data, svc *meta.Service) string {
for _, pkg := range md.Pkgs {
if pkg.RelPath == svc.RelPath {
return pkg.Doc
}
}
return ""
}
// GetLang returns the language specified by the given string, allowing for case insensitivity and common aliases.
func GetLang(lang string) (Lang, error) {
switch strings.TrimSpace(strings.ToLower(lang)) {
case "typescript", "ts":
return LangTypeScript, nil
case "javascript", "js":
return LangJavascript, nil
case "go", "golang":
return LangGo, nil
case "openapi", "swagger", "oas":
return LangOpenAPI, nil
default:
return LangUnknown, ErrUnknownLang
}
}