Define reusable response bodies in .api with typed code mappings, per-route overrides and matching OpenAPI schemas. Add application and callback encoders, early response policy selection, and isolated remote-consumer release verification. Release v0.8.0-preview after full race, vet, build and local MySQL workflow acceptance.
115 lines
3.0 KiB
Go
115 lines
3.0 KiB
Go
package apigen
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"git.yuntuo.hk/k2safe/go-omni/response"
|
|
)
|
|
|
|
type ResponseDefinition struct {
|
|
Name string
|
|
Config response.Config
|
|
Position string
|
|
}
|
|
|
|
func (p *parser) parseResponse() error {
|
|
pos := p.next().position
|
|
name, err := p.ident()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.skip()
|
|
props, err := p.metadata()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
values := map[string]any{}
|
|
for key, value := range props {
|
|
switch key {
|
|
case "successCode", "errorCode":
|
|
if kind := props["codeType"]; kind == "integer" || kind == "int32" || kind == "int64" {
|
|
var number json.Number
|
|
if err = json.Unmarshal([]byte(value), &number); err != nil {
|
|
return fmt.Errorf("%s: %s must be an integer", pos, key)
|
|
}
|
|
values[key] = number
|
|
} else {
|
|
values[key] = value
|
|
}
|
|
case "codes", "messages", "body", "errorBody":
|
|
decoder := json.NewDecoder(strings.NewReader(value))
|
|
decoder.UseNumber()
|
|
var object map[string]any
|
|
if err = decoder.Decode(&object); err != nil || object == nil {
|
|
return fmt.Errorf("%s: %s must be a JSON object", pos, key)
|
|
}
|
|
var extra any
|
|
if decoder.Decode(&extra) != io.EOF {
|
|
return fmt.Errorf("%s: invalid %s mapping", pos, key)
|
|
}
|
|
values[key] = object
|
|
default:
|
|
values[key] = value
|
|
}
|
|
}
|
|
data, err := json.Marshal(values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
decoder.UseNumber()
|
|
decoder.DisallowUnknownFields()
|
|
var cfg response.Config
|
|
if err = decoder.Decode(&cfg); err != nil {
|
|
return fmt.Errorf("%s: response %s: %w", pos, name, err)
|
|
}
|
|
if err = cfg.Validate(); err != nil {
|
|
return fmt.Errorf("%s: %w", pos, err)
|
|
}
|
|
p.spec.Responses = append(p.spec.Responses, ResponseDefinition{Name: name, Config: cfg, Position: pos})
|
|
return nil
|
|
}
|
|
|
|
func responseConfig(spec *Spec, route Route) response.Config {
|
|
name := responseName(route)
|
|
for _, definition := range spec.Responses {
|
|
if definition.Name == name {
|
|
return definition.Config
|
|
}
|
|
}
|
|
return response.Config{}
|
|
}
|
|
|
|
func responseName(route Route) string {
|
|
if route.ResponseFormat == "raw" {
|
|
return route.ErrorResponseFormat
|
|
}
|
|
return route.ResponseFormat
|
|
}
|
|
|
|
func validateResponses(spec *Spec) error {
|
|
seen := map[string]bool{}
|
|
for _, definition := range spec.Responses {
|
|
if !exportedName.MatchString(definition.Name) || seen[definition.Name] {
|
|
return fmt.Errorf("%s: response template requires a unique UpperCamelCase name", definition.Position)
|
|
}
|
|
if err := definition.Config.Validate(); err != nil {
|
|
return fmt.Errorf("%s: %w", definition.Position, err)
|
|
}
|
|
seen[definition.Name] = true
|
|
}
|
|
for _, r := range spec.Routes {
|
|
if r.ResponseFormat != "" && r.ResponseFormat != "raw" && !seen[r.ResponseFormat] {
|
|
return fmt.Errorf("%s: unknown response template %s", r.Position, r.ResponseFormat)
|
|
}
|
|
if r.ErrorResponseFormat != "" && !seen[r.ErrorResponseFormat] {
|
|
return fmt.Errorf("%s: unknown error response template %s", r.Position, r.ErrorResponseFormat)
|
|
}
|
|
}
|
|
return nil
|
|
}
|