Files
fun/template_go.go
T
chiyi 41df889a1a v2.0.0: error-channel APIs, correctness fixes, and server hardening
BREAKING CHANGES:
- Guard interface: Guard(ctx Ctx) error; returning error short-circuits
  (subsequent guards and the method no longer execute)
- Wired[T]() (*T, error); New() may return error; failed wiring is sticky
- BindService/BindGuard/BindRoute return error; routes accept guards
  (guards receive merged query/form params as Ctx.State)
- registration panics after Start; Ctx.Ip honors X-Forwarded-For/X-Real-IP
- internal errors sanitized to fixed client messages

FIXES:
- int64 precision loss: /cell data decoded from raw JSON bytes and
  responses serialized with json.Number (no float64 round-trip)
- enum values range-checked before uint8 conversion (256 no longer
  truncates to 0 and slips through)
- logger: files failing name parsing are no longer deleted; log channel
  never blocks request goroutines; ConfigLogger is race-free;
  logWriterWorker unlock bug fixed
- stream writer panics recovered (process no longer crashes);
  streamDone closed exactly once
- generated Go client emits definitions for pointer-to-enum/struct fields
- anonymous structs rejected at registration; isPrivate safe on empty names
- removed dead Result.Id and RequestInfo.Type

ADDITIONS:
- graceful shutdown (Shutdown), StartOn, server timeouts by default
  (Read 60s/Idle 120s/Write off), SetTimeouts/SetMaxConcurrency
- big-int-safe JSON in the generated TS client (>2^53 as BigInt)
- example/ demo services and cmd/genexample artifact generator
2026-09-03 15:32:07 +08:00

278 lines
6.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package fun
type templateGo struct{}
func (ctx templateGo) genDefaultServiceTemplate() string {
return `package api
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
)
// Result 统一响应结构
type Result[T any] struct {
Code *uint16
Data *T
Msg *string
Status uint8
}
func (r Result[T]) Error() string {
if r.Msg != nil {
return *r.Msg
}
if r.Code != nil {
return fmt.Sprintf("code=%d", *r.Code)
}
return "api: unknown error"
}
// Void 用于无数据返回的方法
type Void = struct{}
// RequestInterceptor 请求前拦截器:可鉴权、加签、改 dto;返回 error 则直接失败
type RequestInterceptor func(serviceName string, methodName string, dto any) error
// ResponseInterceptor 响应后拦截器:可记录日志、埋点、解密;返回 error 则转为失败响应
type ResponseInterceptor func(serviceName string, methodName string, result Result[any]) error
// Client 内联 HTTP 客户端,不依赖外部 funclient 包
type Client struct {
url string
client *http.Client
state map[string]string
requestInterceptors []RequestInterceptor
responseInterceptors []ResponseInterceptor
}
// NewClient 创建客户端
func NewClient(url string) (*Client, error) {
return &Client{
url: strings.TrimRight(url, "/"),
client: &http.Client{},
}, nil
}
// SetHttpClient 替换底层 http.Client
func (c *Client) SetHttpClient(client *http.Client) {
c.client = client
}
// AddRequestInterceptor 注册请求前拦截器
func (c *Client) AddRequestInterceptor(i RequestInterceptor) {
c.requestInterceptors = append(c.requestInterceptors, i)
}
// AddResponseInterceptor 注册响应后拦截器
func (c *Client) AddResponseInterceptor(i ResponseInterceptor) {
c.responseInterceptors = append(c.responseInterceptors, i)
}
// SetState 设置随每个请求携带的状态(如 token),服务端 Guard 可读取
func (c *Client) SetState(state map[string]string) {
c.state = state
}
// Request 发起普通调用
func Request[T any](c *Client, serviceName string, methodName string, dto ...any) Result[T] {
payload := newPayload(serviceName, methodName, dto, c.state)
b, err := json.Marshal(payload)
if err != nil {
return Result[T]{Status: 2, Msg: ptr(err.Error())}
}
for _, i := range c.requestInterceptors {
var dtoVal any
if len(dto) > 0 {
dtoVal = dto[0]
}
if err := i(serviceName, methodName, dtoVal); err != nil {
return Result[T]{Status: 2, Msg: ptr(err.Error())}
}
}
req, err := http.NewRequest(http.MethodPost, c.url+"/cell", bytes.NewReader(b))
if err != nil {
return Result[T]{Status: 2, Msg: ptr(err.Error())}
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return Result[T]{Status: 2, Msg: ptr(err.Error())}
}
defer resp.Body.Close()
var out Result[T]
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return Result[T]{Status: 2, Msg: ptr(err.Error())}
}
var anyData *any
if out.Data != nil {
v := any(*out.Data)
anyData = &v
}
anyResult := Result[any]{Code: out.Code, Data: anyData, Msg: out.Msg, Status: out.Status}
for _, i := range c.responseInterceptors {
if err := i(serviceName, methodName, anyResult); err != nil {
return Result[T]{Status: 2, Msg: ptr(err.Error())}
}
}
return out
}
// Stream 发起流式调用,通过 NDJSON 行逐个推送消息(Streamable HTTP
func Stream[T any](c *Client, serviceName string, methodName string, dto ...any) (<-chan T, error) {
payload := newPayload(serviceName, methodName, dto, c.state)
b, err := json.Marshal(payload)
if err != nil {
return nil, err
}
for _, i := range c.requestInterceptors {
var dtoVal any
if len(dto) > 0 {
dtoVal = dto[0]
}
if err := i(serviceName, methodName, dtoVal); err != nil {
return nil, err
}
}
req, err := http.NewRequest(http.MethodPost, c.url+"/cell", bytes.NewReader(b))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("api: unexpected status %d", resp.StatusCode)
}
anyResult := Result[any]{Status: 0}
for _, i := range c.responseInterceptors {
if err := i(serviceName, methodName, anyResult); err != nil {
resp.Body.Close()
return nil, err
}
}
ch := make(chan T)
go func() {
defer resp.Body.Close()
defer close(ch)
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
var msg T
if err := json.Unmarshal([]byte(line), &msg); err != nil {
continue
}
ch <- msg
}
}()
return ch, nil
}
func newPayload(serviceName string, methodName string, dto []any, state map[string]string) map[string]any {
payload := map[string]any{
"serviceName": serviceName,
"methodName": methodName,
}
if len(dto) > 0 {
payload["data"] = dto[0]
}
if len(state) > 0 {
payload["state"] = state
}
return payload
}
func ptr(s string) *string { return &s }
type Api struct {
{{- range .GenServiceList}}
{{.ServiceName}} *{{.ServiceName}}
{{- end}}
*Client
}
func CreateApi(url string) (Api, error) {
apiClient, err := NewClient(url)
return Api{
{{- range .GenServiceList}}
{{.ServiceName}}: New{{.ServiceName}}(apiClient),
{{- end}}
Client: apiClient,
}, err
}`
}
func (ctx templateGo) genServiceTemplate() string {
return `package api
type {{.ServiceName}} struct {
*Client
}
func New{{.ServiceName}}(client *Client) *{{.ServiceName}} {
return &{{.ServiceName}}{
Client: client,
}
}
{{- $serviceName := .ServiceName }}
{{- range .GenMethodTypeList}}
{{if .IsStream }}func (ctx *{{$serviceName}}) {{.MethodName}}({{.DtoText}}) (<-chan {{.GenericTypeText}}, error) {
return Stream[{{.GenericTypeText}}](ctx.Client, "{{$serviceName}}", "{{.MethodName}}"{{.ArgsText}})
}{{else}}func (ctx *{{$serviceName}}) {{.MethodName}}({{.DtoText}}) {{.ReturnValueText}} {
return Request[{{.GenericTypeText}}](ctx.Client, "{{$serviceName}}", "{{.MethodName}}"{{.ArgsText}})
}{{end}}
{{- end}}`
}
func (ctx templateGo) genStructTemplate() string {
return `package api
type {{.Name}} struct{
{{- range .GenClassFieldType}}
{{.Name}} {{.Type}} {{.Tag}}
{{- end}}
}`
}
func (ctx templateGo) genEnumTemplate() string {
return `package api
type {{.Name}} uint8
{{$enumName := .Name}}
const (
{{- range $index, $element := .Names}}
{{$element}}{{if eq $index 0}} {{$enumName}} = iota{{end}}
{{- end}}
)
func ({{.Name}}) Values() []{{.Name}} {
return []{{.Name}}{
{{- range $index, $element := .Names}}
{{$element}},
{{- end}}
}
}
{{if .DisplayNames}}
func ({{.Name}}) DisplayNames() []string {
return []string{
{{- range $index, $element := .DisplayNames}}
"{{$element}}",
{{- end}}
}
}
{{end}}`
}