Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a67bddf
Add kitchen-sink JSON-RPC golden test pinning full generated surface
raphael Jun 12, 2026
ebcaf8c
Fail fast on codegen invariant violations with a bug-report boundary
raphael Jun 12, 2026
f0856cf
Trim dead code and collapse duplication in shared codegen core
raphael Jun 12, 2026
13c64bf
Unify service-layer view pairs and single-source JSON-RPC WebSocket fact
raphael Jun 12, 2026
d6cc176
Collapse mirror builders and sweep dead paths in HTTP and gRPC codegen
raphael Jun 12, 2026
2bf1b09
Hoist cross-transport CLI, example, and package-resolution duplication
raphael Jun 12, 2026
c51b832
Replace JSON-RPC template-source surgery with structural variation po…
raphael Jun 12, 2026
3fb7999
Drive JSON-RPC file paths, titles, and CLI imports from a transport tag
raphael Jun 12, 2026
8cf53fe
Unify JSON-RPC SSE server streams behind one generated base type
raphael Jun 12, 2026
a123577
Make protobuf message wrapping an explicit contract
raphael Jun 12, 2026
7256035
Re-converge the protobuf transform onto the shared engine via hooks
raphael Jun 12, 2026
df008bf
Make the validation engine pure with expr.EffectiveValidation
raphael Jun 12, 2026
ec30574
Make HTTP emission idempotent and gRPC analyze read-only
raphael Jun 12, 2026
de7265a
Make HTTP analyze read-only over the design tree
raphael Jun 12, 2026
fe20b74
Sanction one design normalization pass and pin codegen purity in CI
raphael Jun 13, 2026
dc64513
Make examples a pure function of the design
raphael Jun 13, 2026
3cf52f3
Anchor OpenAPI schema examples to design identities
raphael Jun 13, 2026
2e61efe
Anchor OpenAPI v3 parameter examples to their payload fields
raphael Jun 13, 2026
5f34a1c
Anchor the OpenAPI v3 schemafier example streams
raphael Jun 13, 2026
85fea68
Anchor request and response body example streams on endpoint identity
raphael Jun 13, 2026
622d193
Preserve schema and view output contracts
raphael Jun 13, 2026
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
17 changes: 16 additions & 1 deletion cmd/goa/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ func (g *Generator) Write(_ bool) error {
codegen.SimpleImport("fmt"),
codegen.SimpleImport("os"),
codegen.SimpleImport("path/filepath"),
codegen.SimpleImport("runtime/debug"),
codegen.SimpleImport("sort"),
codegen.SimpleImport("strconv"),
codegen.SimpleImport("strings"),
Expand Down Expand Up @@ -366,7 +367,7 @@ const mainT = `func main() {
{{- end }}

startGenerate := time.Now()
outputs, err := generator.Generate(*out, {{ printf "%q" .Command }}, *debug)
outputs, err := generate(*out, {{ printf "%q" .Command }}, *debug)
if err != nil {
fail(err.Error())
}
Expand All @@ -377,6 +378,20 @@ const mainT = `func main() {
fmt.Println(strings.Join(outputs, "\n"))
}

// generate runs code generation and converts panics into a bug report
// request: Goa generators panic on internal invariant violations, and this
// recover is the single boundary turning them into actionable output. Design
// errors never reach this point; eval.RunDSL reports them before generation.
func generate(out, cmd string, dbg bool) ([]string, error) {
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "panic: %v\n\n%s\n", r, debug.Stack())
fail("This is a bug in Goa, please report it at https://github.com/goadesign/goa/issues and include the stack trace above together with the design that triggered it.\n")
}
}()
return generator.Generate(out, cmd, dbg)
}

func fail(msg string, vals ...any) {
fmt.Fprintf(os.Stderr, msg, vals...)
os.Exit(1)
Expand Down
121 changes: 121 additions & 0 deletions codegen/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,35 @@ type (
CheckErr bool
}

// FlagArgData describes a payload initialization argument from which a
// command-line flag and the code that loads the flag value into the
// corresponding payload builder field are generated.
FlagArgData struct {
// Name is the argument variable name used to derive the flag name and
// the name of the local variable holding the flag value.
Name string
// TypeName is the argument Go type name.
TypeName string
// TypeRef is the reference to the argument type.
TypeRef string
// FieldName is the name of the payload field initialized with the
// argument value if any.
FieldName string
// Description is the flag help text.
Description string
// Required is true if the flag is required.
Required bool
// Example is an example value for the flag.
Example any
// DefaultValue is the default value of the argument if any.
DefaultValue any
// Validate contains the validation code for the argument value if any.
Validate string
// OmitField if true generates the flag without a corresponding payload
// builder field.
OmitField bool
}

// FieldData contains the data needed to generate the code that initializes a
// field in the method payload type.
FieldData struct {
Expand Down Expand Up @@ -245,6 +274,98 @@ func BuildSubcommandData(data *service.Data, m *service.MethodData, buildFunctio
return sub
}

// EndpointParserFile returns the file that implements the command line parser
// that builds the client endpoint and payload necessary to perform a request.
// The parse section renders the transport-specific ParseEndpoint function.
func EndpointParserFile(
path, title string,
specs []*codegen.ImportSpec,
data []*CommandData,
parseSection *codegen.SectionTemplate,
) *codegen.File {
sections := make([]*codegen.SectionTemplate, 0, 4+len(data))
sections = append(sections,
codegen.Header(title, "cli", specs),
UsageCommands(data),
UsageExamples(data),
parseSection,
)
for _, cmd := range data {
sections = append(sections, CommandUsage(cmd))
}
return &codegen.File{Path: path, SectionTemplates: sections}
}

// MakeFlags returns the flag data generated from the given payload
// initialization arguments along with the data for the function that builds
// the method payload from the corresponding flag values. payload and
// payloadRef describe the method payload type, pinit - if not nil - describes
// the payload constructor invoked by the build function.
func MakeFlags(
svcn string,
m *service.MethodData,
args []*FlagArgData,
payload expr.DataType,
payloadRef string,
pinit *PayloadInitData,
) ([]*FlagData, *BuildFunctionData) {
var (
fdata = make([]*FieldData, 0, len(args)) // preallocate
flags = make([]*FlagData, len(args))
params = make([]string, len(args))
check bool
)
for i, arg := range args {
f := NewFlagData(svcn, m.Name, arg.Name, arg.TypeName, arg.Description, arg.Required, arg.Example, arg.DefaultValue)
flags[i] = f
params[i] = f.FullName
if arg.OmitField {
continue
}
code, chek := FieldLoadCode(f, arg.Name, arg.TypeName, arg.Validate, arg.DefaultValue, payload, payloadRef)
check = check || chek
tn := arg.TypeRef
if f.Type == "JSON" {
// We need to declare the variable without
// a pointer to be able to unmarshal the JSON
// using its address.
tn = arg.TypeName
}
fdata = append(fdata, &FieldData{
Name: arg.Name,
VarName: arg.Name,
TypeRef: tn,
Init: code,
})
}

return flags, &BuildFunctionData{
Name: "Build" + m.VarName + "Payload",
ActualParams: params,
FormalParams: params,
ServiceName: svcn,
MethodName: m.Name,
ResultType: payloadRef,
Fields: fdata,
PayloadInit: pinit,
CheckErr: check,
}
}

// PayloadBuildersFile returns the file that contains the payload constructors
// that use the command flag values as arguments.
func PayloadBuildersFile(path, title string, specs []*codegen.ImportSpec, data *CommandData) *codegen.File {
sections := []*codegen.SectionTemplate{
codegen.Header(title, "client", specs),
}
for _, sub := range data.Subcommands {
if sub.BuildFunction != nil {
sections = append(sections, PayloadBuilderSection(sub.BuildFunction))
}
}
return &codegen.File{Path: path, SectionTemplates: sections}
}

// UsageCommands builds a section template that generates a help text showing
// the list of allowed commands and sub-commands.
func UsageCommands(data []*CommandData) *codegen.SectionTemplate {
Expand Down
33 changes: 20 additions & 13 deletions codegen/example/example_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@ func ServerFiles(genpkg string, root *expr.RootExpr, services *service.ServicesD
return fw
}

// APIPkg returns a unique package name for the example API implementation
// package derived from the API name. The name is registered with the given
// scope so subsequent calls return distinct names.
func APIPkg(root *expr.RootExpr, scope *codegen.NameScope) string {
return scope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api")
}

// RootPath returns the Go import path of the project root computed from the
// generated code package import path genpkg. It returns "." if genpkg has no
// parent path.
func RootPath(genpkg string) string {
// genpkg is created by path.Join so the separator is / regardless of operating system
if idx := strings.LastIndex(genpkg, "/"); idx > 0 {
return genpkg[:idx]
}
return "."
}

// exampleSvrMain returns the default main function for the given server
// expression.
func exampleSvrMain(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr, services *service.ServicesData) *codegen.File {
Expand Down Expand Up @@ -62,19 +80,8 @@ func exampleSvrMain(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr, se
}
interPkg := scope.Unique("interceptors", "ex")

var (
rootPath string
apiPkg string
)
{
// genpkg is created by path.Join so the separator is / regardless of operating system
idx := strings.LastIndex(genpkg, string("/"))
rootPath = "."
if idx > 0 {
rootPath = genpkg[:idx]
}
apiPkg = scope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api")
}
rootPath := RootPath(genpkg)
apiPkg := APIPkg(root, scope)
specs = append(specs, &codegen.ImportSpec{Path: rootPath, Name: apiPkg})
if hasInterceptors {
specs = append(specs, &codegen.ImportSpec{Path: path.Join(rootPath, "interceptors"), Name: interPkg})
Expand Down
4 changes: 2 additions & 2 deletions codegen/generator/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ func Example(genpkg string, roots []eval.Root) ([]*codegen.File, error) {

// JSON-RPC
if len(r.API.JSONRPC.Services) > 0 {
jsonrpcServices := httpcodegen.NewServicesData(services, &r.API.JSONRPC.HTTPExpr)
jsonrpcServices := httpcodegen.NewJSONRPCServicesData(services, &r.API.JSONRPC.HTTPExpr)
if fs := jsonrpccodegen.ExampleServerFiles(genpkg, jsonrpcServices, files); len(fs) > 0 {
files = append(files, fs...)
}
if fs := jsonrpccodegen.ExampleCLIFiles(genpkg, jsonrpcServices); len(fs) > 0 {
if fs := httpcodegen.ExampleCLIFiles(genpkg, jsonrpcServices); len(fs) > 0 {
files = append(files, fs...)
}
}
Expand Down
12 changes: 11 additions & 1 deletion codegen/generator/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"goa.design/goa/v3/codegen"
"goa.design/goa/v3/eval"
"goa.design/goa/v3/expr"
"golang.org/x/tools/go/packages"
)

Expand Down Expand Up @@ -100,13 +101,22 @@ func Generate(dir, cmd string, debug bool) (outputs []string, err1 error) {
}
}

// 4. Run the code pre generation plugins.
// 4. Run the code pre generation plugins then normalize the design
// roots. NormalizeRoot is the only sanctioned design mutation past eval
// finalization; it runs after the prepare plugins so plugin contributed
// endpoints are normalized too and before the generators so they all
// observe the same read-only design tree.
{
start := time.Now()
err := codegen.RunPluginsPrepare(cmd, genpkg, roots)
if err != nil {
return nil, err
}
for _, root := range roots {
if r, ok := root.(*expr.RootExpr); ok {
codegen.NormalizeRoot(r)
}
}
if debug {
fmt.Fprintf(os.Stderr, "[TIMING] [generate] Stage 4: Run pre-generation plugins took %v\n", time.Since(start))
}
Expand Down
Loading
Loading