Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ cover.out
**/.DS_Store

jsonrpc/integration_tests/tests/testdata/runs/*

.kiro/
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ files if they do not already exist.
- Use multi‑line `if` blocks; target ~80‑column lines when practical.
- Struct/composite literals: break long/named fields onto one field per line with trailing commas; close brace on its own line.

### Slices, Maps, and Validation Contracts

- Do not rely on `nil` vs empty slices or maps to encode presence for required
fields. Goa’s generated Go structs use `omitempty`, and gRPC/JSON treat both
`nil` and empty slices/maps as “missing” for required properties. If a field
is required at the contract level, model it as a non-collection scalar (or
use an explicit presence flag), not as “non‑empty slice implies present”.
Conversely, if an array/map may legitimately be empty, do **not** mark it
as required in the DSL—make the container required and the collection
optional.

## Testing Guidelines
- Write table‑driven tests in `*_test.go` using `testing` (optionally `testify`).
- Name tests `TestXxx`; keep unit tests fast and deterministic.
Expand Down
33 changes: 22 additions & 11 deletions dsl/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ func API(name string, fn func()) *expr.APIExpr {

// Title sets the API title. It is used by the generated OpenAPI specification.
//
// Title must appear in an API expression.
// Title must appear in an API expression or any expression implementing
// TitleHolder.
//
// Title accepts a single string argument.
//
Expand All @@ -66,16 +67,20 @@ func API(name string, fn func()) *expr.APIExpr {
// Title("divider API")
// })
func Title(val string) {
if s, ok := eval.Current().(*expr.APIExpr); ok {
s.Title = val
return
switch e := eval.Current().(type) {
case *expr.APIExpr:
e.Title = val
case expr.TitleHolder:
e.SetTitle(val)
default:
eval.IncompatibleDSL()
}
eval.IncompatibleDSL()
}

// Version specifies the API version. One design describes one version.
//
// Version must appear in an API expression.
// Version must appear in an API expression or any expression implementing
// VersionHolder.
//
// Version accepts a single string argument.
//
Expand All @@ -85,11 +90,14 @@ func Title(val string) {
// Version("1.0")
// })
func Version(ver string) {
if s, ok := eval.Current().(*expr.APIExpr); ok {
s.Version = ver
return
switch e := eval.Current().(type) {
case *expr.APIExpr:
e.Version = ver
case expr.VersionHolder:
e.SetVersion(ver)
default:
eval.IncompatibleDSL()
}
eval.IncompatibleDSL()
}

// Contact sets the API contact information.
Expand Down Expand Up @@ -278,7 +286,8 @@ func Email(email string) {

// URL sets the contact, license or external documentation URL.
//
// URL must appear in Contact, License or Docs.
// URL must appear in Contact, License, Docs, or any expression implementing
// URLHolder.
//
// URL accepts a single argument which is the URL.
//
Expand All @@ -295,6 +304,8 @@ func URL(url string) {
def.URL = url
case *expr.DocsExpr:
def.URL = url
case expr.URLHolder:
def.SetURL(url)
default:
eval.IncompatibleDSL()
}
Expand Down
7 changes: 5 additions & 2 deletions dsl/description.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import (
// Description sets the expression description.
//
// Description may appear in API, Docs, Type or Attribute.
// Description may also appear in Response and Files.
// Description may also appear in Response and Files, or any expression
// implementing DescriptionHolder.
//
// Description accepts one arguments: the description string.
// Description accepts one argument: the description string.
//
// Example:
//
Expand Down Expand Up @@ -47,6 +48,8 @@ func Description(d string) {
e.Description = d
case *expr.InterceptorExpr:
e.Description = d
case expr.DescriptionHolder:
e.SetDescription(d)
default:
eval.IncompatibleDSL()
}
Expand Down
40 changes: 31 additions & 9 deletions dsl/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,26 +229,48 @@ func Temporary() {
attr.AddMeta("goa:error:temporary")
}

// Timeout qualifies an error type as describing errors due to timeouts.
// Timeout qualifies an error type as describing errors due to timeouts, or
// sets a timeout duration on expressions implementing TimeoutHolder.
//
// Timeout must appear in a Error expression.
// When used in an Error expression, Timeout takes no argument and marks the
// error as a timeout error.
//
// Timeout takes no argument.
// When used in an expression implementing TimeoutHolder, Timeout takes a
// duration string (e.g., "30s", "1m", "500ms").
//
// Example:
// Example (error timeout):
//
// var _ = Service("divider", func() {
// Error("request_timeout", func() {
// Timeout()
// })
// })
func Timeout() {
attr, ok := eval.Current().(*expr.AttributeExpr)
if !ok {
//
// Example (duration timeout):
//
// Registry("corp", func() {
// URL("https://registry.corp.internal")
// Timeout("30s")
// })
func Timeout(args ...string) {
switch e := eval.Current().(type) {
case *expr.AttributeExpr:
if len(args) > 0 {
eval.ReportError("Timeout in Error expression takes no arguments")
return
}
e.AddMeta("goa:error:timeout")
case expr.TimeoutHolder:
if len(args) != 1 {
eval.ReportError("Timeout requires a duration string argument")
return
}
if err := e.SetTimeout(args[0]); err != nil {
eval.ReportError("invalid timeout: %s", err)
}
default:
eval.IncompatibleDSL()
return
}
attr.AddMeta("goa:error:timeout")
}

// Fault qualifies an error type as describing errors due to a server-side
Expand Down
2 changes: 2 additions & 0 deletions dsl/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ func Security(args ...any) {
actual.Requirements = append(actual.Requirements, security)
case *expr.APIExpr:
actual.Requirements = append(actual.Requirements, security)
case expr.SecurityHolder:
actual.AddSecurityRequirement(security)
default:
eval.IncompatibleDSL()
return
Expand Down
35 changes: 35 additions & 0 deletions expr/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,41 @@ type (
// URL to documentation.
URL string `json:"url,omitempty"`
}

// URLHolder is an interface that allows expression types to receive
// a URL. Types implementing this interface can use the URL() DSL
// function to set a URL.
URLHolder interface {
SetURL(string)
}

// DescriptionHolder is an interface that allows expression types to
// receive a description. Types implementing this interface can use
// the Description() DSL function to set a description.
DescriptionHolder interface {
SetDescription(string)
}

// TitleHolder is an interface that allows expression types to receive
// a title. Types implementing this interface can use the Title() DSL
// function to set a title.
TitleHolder interface {
SetTitle(string)
}

// VersionHolder is an interface that allows expression types to
// receive a version. Types implementing this interface can use the
// Version() DSL function to set a version.
VersionHolder interface {
SetVersion(string)
}

// TimeoutHolder is an interface that allows expression types to
// receive a timeout duration. Types implementing this interface can
// use the Timeout() DSL function to set a timeout.
TimeoutHolder interface {
SetTimeout(string) error
}
)

// NewAPIExpr initializes an API expression.
Expand Down
7 changes: 7 additions & 0 deletions expr/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ const (
)

type (
// SecurityHolder is an interface that allows expression types to receive
// security requirements. Types implementing this interface can use the
// Security() DSL function to add security schemes.
SecurityHolder interface {
AddSecurityRequirement(*SecurityExpr)
}

// SecurityExpr defines a security requirement.
SecurityExpr struct {
// Schemes is the list of security schemes used for this
Expand Down
Loading