Skip to content

Commit 2b6cd3f

Browse files
committed
Add encore.dev/types/option package
This adds a new `encore.dev/types/option` package for Encore.go, with a new type option.Option[T] for representing optional values. This improves Encore's support for representing optional types in APIs. It's also something that we've internally found to be useful again and again in large code bases, where it's difficult to keep track of whether a pointer value *T means the type is nullable or not.
1 parent c577689 commit 2b6cd3f

File tree

49 files changed

+6731
-11315
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+6731
-11315
lines changed

cli/daemon/schema.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ func (r *schemaRenderer) renderType(typ *schema.Type) {
4949
r.renderNamed(typ.Named)
5050
case *schema.Type_Pointer:
5151
r.renderType(typ.Pointer.Base)
52+
case *schema.Type_Option:
53+
r.WriteNil()
5254
case *schema.Type_Union:
5355
r.renderType(typ.Union.Types[0])
5456
case *schema.Type_Literal:

docs/go/primitives/defining-apis.md

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -284,22 +284,29 @@ type CreateBlogPost struct {
284284
}
285285
```
286286

287+
### Optional types
288+
289+
Encore supports optional types using the `option.Option[T]` type from the `encore.dev/types/option` package.
290+
This can be used in request and response schemas to indicate that the value is not always set.
291+
292+
See the [package documentation](https://pkg.go.dev/encore.dev/types/option) for more information on usage.
293+
287294
### Supported types
288295
The table below lists the data types supported by each HTTP message location.
289296

290-
| Type | Header | Path | Query | Body |
291-
| --------------- | ------ | ---- | ----- | ---- |
292-
| bool | X | X | X | X |
293-
| numeric | X | X | X | X |
294-
| string | X | X | X | X |
295-
| time.Time | X | X | X | X |
296-
| uuid.UUID | X | X | X | X |
297-
| json.RawMessage | X | X | X | X |
298-
| list | | | X | X |
299-
| struct | | | | X |
300-
| map | | | | X |
301-
| pointer | | | | X |
302-
297+
| Type | Header | Path | Query | Body |
298+
| ---------------- | ------ | ---- | ----- | ---- |
299+
| bool | X | X | X | X |
300+
| numeric | X | X | X | X |
301+
| string | X | X | X | X |
302+
| time.Time | X | X | X | X |
303+
| uuid.UUID | X | X | X | X |
304+
| json.RawMessage | X | X | X | X |
305+
| option.Option[T] | X | | X | X |
306+
| pointer | X | | X | X |
307+
| list | X | | X | X |
308+
| struct | | | | X |
309+
| map | | | | X |
303310

304311
## Sensitive data
305312

e2e-tests/testdata/echo/endtoend/endtoend.go

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@ import (
55
"encoding/json"
66
"fmt"
77
"math/rand"
8+
89
// "net/http"
910
// "net/http/httptest"
1011
"os"
1112
"reflect"
13+
1214
// "strings"
1315
"time"
1416

1517
"encore.app/test"
1618
"encore.dev/beta/errs"
1719
"encore.dev/rlog"
20+
"encore.dev/types/option"
1821
"encore.dev/types/uuid"
1922
)
2023

@@ -92,35 +95,38 @@ func GeneratedWrappersEndToEndTest(ctx context.Context) (err error) {
9295
r.Read(queryBytes)
9396
r.Read(bodyBytes)
9497
params := &test.MarshallerTest[int]{
95-
HeaderBoolean: r.Float32() > 0.5,
96-
HeaderInt: r.Int(),
97-
HeaderFloat: r.Float64(),
98-
HeaderString: "header string",
99-
HeaderBytes: headerBytes,
100-
HeaderTime: time.Now().Truncate(time.Second),
101-
HeaderJson: json.RawMessage("{\"hello\":\"world\"}"),
102-
HeaderUUID: newUUID(),
103-
HeaderUserID: "432",
104-
QueryBoolean: r.Float32() > 0.5,
105-
QueryInt: r.Int(),
106-
QueryFloat: r.Float64(),
107-
QueryString: "query string",
108-
QueryBytes: headerBytes,
109-
QueryTime: time.Now().Add(time.Duration(rand.Intn(1024)) * time.Hour).Truncate(time.Second),
110-
QueryJson: json.RawMessage("true"),
111-
QueryUUID: newUUID(),
112-
QueryUserID: "9udfa",
113-
QuerySlice: []int{r.Int(), r.Int(), r.Int(), r.Int()},
114-
BodyBoolean: r.Float32() > 0.5,
115-
BodyInt: r.Int(),
116-
BodyFloat: r.Float64(),
117-
BodyString: "body string",
118-
BodyBytes: bodyBytes,
119-
BodyTime: time.Now().Add(time.Duration(rand.Intn(1024)) * time.Hour).Truncate(time.Second),
120-
BodyJson: json.RawMessage("null"),
121-
BodyUUID: newUUID(),
122-
BodyUserID: "✉️",
123-
BodySlice: []int{r.Int(), r.Int(), r.Int(), r.Int(), r.Int(), r.Int()},
98+
HeaderBoolean: r.Float32() > 0.5,
99+
HeaderInt: r.Int(),
100+
HeaderFloat: r.Float64(),
101+
HeaderString: "header string",
102+
HeaderBytes: headerBytes,
103+
HeaderTime: time.Now().Truncate(time.Second),
104+
HeaderJson: json.RawMessage("{\"hello\":\"world\"}"),
105+
HeaderUUID: newUUID(),
106+
HeaderUserID: "432",
107+
HeaderOption: option.Some("test"),
108+
QueryBoolean: r.Float32() > 0.5,
109+
QueryInt: r.Int(),
110+
QueryFloat: r.Float64(),
111+
QueryString: "query string",
112+
QueryBytes: headerBytes,
113+
QueryTime: time.Now().Add(time.Duration(rand.Intn(1024)) * time.Hour).Truncate(time.Second),
114+
QueryJson: json.RawMessage("true"),
115+
QueryUUID: newUUID(),
116+
QueryUserID: "9udfa",
117+
QuerySlice: []int{r.Int(), r.Int(), r.Int(), r.Int()},
118+
QuerySliceOptions: []option.Option[int]{option.Some(r.Int()), option.None[int](), option.Some(r.Int())},
119+
BodyBoolean: r.Float32() > 0.5,
120+
BodyInt: r.Int(),
121+
BodyFloat: r.Float64(),
122+
BodyString: "body string",
123+
BodyBytes: bodyBytes,
124+
BodyTime: time.Now().Add(time.Duration(rand.Intn(1024)) * time.Hour).Truncate(time.Second),
125+
BodyJson: json.RawMessage("null"),
126+
BodyUUID: newUUID(),
127+
BodyUserID: "✉️",
128+
BodySlice: []int{r.Int(), r.Int(), r.Int(), r.Int(), r.Int(), r.Int()},
129+
BodyOption: option.Some(r.Int()),
124130
}
125131
mResp, err := test.MarshallerTestHandler(ctx, params)
126132
assert(err, nil, "Expected no error from the marshaller test")

e2e-tests/testdata/echo/test/endpoints.go

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
encore "encore.dev"
1212
"encore.dev/beta/auth"
1313
"encore.dev/beta/errs"
14+
"encore.dev/types/option"
1415
"encore.dev/types/uuid"
1516
)
1617

@@ -98,35 +99,38 @@ func RestStyleAPI(ctx context.Context, objType int, name string, params *RestPar
9899
}
99100

100101
type MarshallerTest[A any] struct {
101-
HeaderBoolean bool `header:"x-boolean"`
102-
HeaderInt int `header:"x-int"`
103-
HeaderFloat float64 `header:"x-float"`
104-
HeaderString string `header:"x-string"`
105-
HeaderBytes []byte `header:"x-bytes"`
106-
HeaderTime time.Time `header:"x-time"`
107-
HeaderJson json.RawMessage `header:"x-json"`
108-
HeaderUUID uuid.UUID `header:"x-uuid"`
109-
HeaderUserID auth.UID `header:"x-user-id"`
110-
QueryBoolean bool `qs:"boolean"`
111-
QueryInt int `qs:"int"`
112-
QueryFloat float64 `qs:"float"`
113-
QueryString string `qs:"string"`
114-
QueryBytes []byte `qs:"bytes"`
115-
QueryTime time.Time `qs:"time"`
116-
QueryJson json.RawMessage `qs:"json"`
117-
QueryUUID uuid.UUID `qs:"uuid"`
118-
QueryUserID auth.UID `qs:"user-id"`
119-
QuerySlice []A `qs:"slice"`
120-
BodyBoolean bool `json:"boolean"`
121-
BodyInt int `json:"int"`
122-
BodyFloat float64 `json:"float"`
123-
BodyString string `json:"string"`
124-
BodyBytes []byte `json:"bytes"`
125-
BodyTime time.Time `json:"time"`
126-
BodyJson json.RawMessage `json:"json"`
127-
BodyUUID uuid.UUID `json:"uuid"`
128-
BodyUserID auth.UID `json:"user-id"`
129-
BodySlice []A `json:"slice"`
102+
HeaderBoolean bool `header:"x-boolean"`
103+
HeaderInt int `header:"x-int"`
104+
HeaderFloat float64 `header:"x-float"`
105+
HeaderString string `header:"x-string"`
106+
HeaderBytes []byte `header:"x-bytes"`
107+
HeaderTime time.Time `header:"x-time"`
108+
HeaderJson json.RawMessage `header:"x-json"`
109+
HeaderUUID uuid.UUID `header:"x-uuid"`
110+
HeaderUserID auth.UID `header:"x-user-id"`
111+
HeaderOption option.Option[string] `header:"x-option"`
112+
QueryBoolean bool `qs:"boolean"`
113+
QueryInt int `qs:"int"`
114+
QueryFloat float64 `qs:"float"`
115+
QueryString string `qs:"string"`
116+
QueryBytes []byte `qs:"bytes"`
117+
QueryTime time.Time `qs:"time"`
118+
QueryJson json.RawMessage `qs:"json"`
119+
QueryUUID uuid.UUID `qs:"uuid"`
120+
QueryUserID auth.UID `qs:"user-id"`
121+
QuerySlice []A `qs:"slice"`
122+
QuerySliceOptions []option.Option[A] `qs:"slice-options"`
123+
BodyBoolean bool `json:"boolean"`
124+
BodyInt int `json:"int"`
125+
BodyFloat float64 `json:"float"`
126+
BodyString string `json:"string"`
127+
BodyBytes []byte `json:"bytes"`
128+
BodyTime time.Time `json:"time"`
129+
BodyJson json.RawMessage `json:"json"`
130+
BodyUUID uuid.UUID `json:"uuid"`
131+
BodyUserID auth.UID `json:"user-id"`
132+
BodySlice []A `json:"slice"`
133+
BodyOption option.Option[A] `json:"option"`
130134
}
131135

132136
// MarshallerTestHandler allows us to test marshalling of all the inbuilt types in all

e2e-tests/testdata/tsapp/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

e2e-tests/testdata/tsapp/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
"vitest": "^3.1.3"
1515
},
1616
"dependencies": {
17-
"encore.dev": "1.50.0"
17+
"encore.dev": "file:/Users/andre/src/github.com/encoredev/encore/runtimes/js/encore.dev"
1818
},
1919
"optionalDependencies": {
2020
"@rollup/rollup-linux-x64-gnu": "^4.13.0"
2121
}
22-
}
22+
}

internal/gocodegen/helpers.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ func ConvertSchemaToJenType(typ *schema.Type, md *meta.Data) *Statement {
7979
case *schema.Type_Pointer:
8080
return Op("*").Add(ConvertSchemaToJenType(typ.Pointer.Base, md))
8181

82+
case *schema.Type_Option:
83+
return Qual("encore.dev/types/option", "Option").Types(ConvertSchemaToJenType(typ.Option.Value, md))
84+
8285
case *schema.Type_TypeParameter:
8386
return Id(md.Decls[typ.TypeParameter.DeclId].TypeParams[typ.TypeParameter.ParamIdx].Name)
8487

0 commit comments

Comments
 (0)