From e487a9b15a0c19082cfe9b41661276e6ffdffd03 Mon Sep 17 00:00:00 2001 From: Fredrik Enestad Date: Mon, 25 Nov 2024 14:00:16 +0100 Subject: [PATCH] tsparser: support root path for enpoints (#1591) --- .../testdata/tsapp/expected_golang.go | 41 +++++++++++++++++++ .../testdata/tsapp/expected_javascript.js | 21 ++++++++++ .../testdata/tsapp/expected_typescript.ts | 38 +++++++++++++++++ internal/clientgen/testdata/tsapp/input.ts | 5 +++ tsparser/src/parser/respath.rs | 3 +- 5 files changed, 107 insertions(+), 1 deletion(-) diff --git a/internal/clientgen/testdata/tsapp/expected_golang.go b/internal/clientgen/testdata/tsapp/expected_golang.go index c1a90522e4..a6c8e41e6e 100644 --- a/internal/clientgen/testdata/tsapp/expected_golang.go +++ b/internal/clientgen/testdata/tsapp/expected_golang.go @@ -104,10 +104,20 @@ type SvcRequest struct { headerNum float64 `encore:"optional" header:"num,optional"` } +type SvcRequest struct { + foo float64 `encore:"optional"` // Foo is good + baz string // Baz is better + queryFoo bool `encore:"optional" query:"foo,optional"` + queryBar string `encore:"optional" query:"bar,optional"` + headerBaz string `encore:"optional" header:"baz,optional"` + headerNum float64 `encore:"optional" header:"num,optional"` +} + // SvcClient Provides you access to call public and authenticated APIs on svc. The concrete implementation is svcClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type SvcClient interface { dummy(ctx context.Context, params SvcRequest) error + root(ctx context.Context, params SvcRequest) error } type svcClient struct { @@ -147,6 +157,37 @@ func (c *svcClient) dummy(ctx context.Context, params SvcRequest) error { return err } +func (c *svcClient) root(ctx context.Context, params SvcRequest) error { + // Convert our params into the objects we need for the request + reqEncoder := &serde{} + + headers := http.Header{ + "baz": {reqEncoder.FromString(params.headerBaz)}, + "num": {reqEncoder.FromFloat64(params.headerNum)}, + } + + queryString := url.Values{ + "bar": {reqEncoder.FromString(params.queryBar)}, + "foo": {reqEncoder.FromBool(params.queryFoo)}, + } + + if reqEncoder.LastError != nil { + return fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) + } + + // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) + body := struct { + foo float64 `json:"foo"` + baz string `json:"baz"` + }{ + baz: params.baz, + foo: params.foo, + } + + _, err := callAPI(ctx, c.base, "POST", fmt.Sprintf("/?%s", queryString.Encode()), headers, body, nil) + return err +} + // HTTPDoer is an interface which can be used to swap out the default // HTTP client (http.DefaultClient) with your own custom implementation. // This can be used to inject middleware or mock responses during unit tests. diff --git a/internal/clientgen/testdata/tsapp/expected_javascript.js b/internal/clientgen/testdata/tsapp/expected_javascript.js index 0c06a07e3b..a931415926 100644 --- a/internal/clientgen/testdata/tsapp/expected_javascript.js +++ b/internal/clientgen/testdata/tsapp/expected_javascript.js @@ -65,6 +65,27 @@ class SvcServiceClient { await this.baseClient.callAPI("POST", `/dummy`, JSON.stringify(body), {headers, query}) } + + async root(params) { + // Convert our params into the objects we need for the request + const headers = makeRecord({ + baz: params.headerBaz, + num: params.headerNum === undefined ? undefined : String(params.headerNum), + }) + + const query = makeRecord({ + bar: params.queryBar, + foo: params.queryFoo === undefined ? undefined : String(params.queryFoo), + }) + + // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) + const body = { + baz: params.baz, + foo: params.foo, + } + + await this.baseClient.callAPI("POST", `/`, JSON.stringify(body), {headers, query}) + } } export const svc = { diff --git a/internal/clientgen/testdata/tsapp/expected_typescript.ts b/internal/clientgen/testdata/tsapp/expected_typescript.ts index b6f7a6cacd..a60dcf15e3 100644 --- a/internal/clientgen/testdata/tsapp/expected_typescript.ts +++ b/internal/clientgen/testdata/tsapp/expected_typescript.ts @@ -90,6 +90,23 @@ export namespace svc { headerNum?: number } + export interface Request { + /** + * Foo is good + */ + foo?: number + + /** + * Baz is better + */ + baz: string + + queryFoo?: boolean + queryBar?: string + headerBaz?: string + headerNum?: number + } + export class ServiceClient { private baseClient: BaseClient @@ -117,6 +134,27 @@ export namespace svc { await this.baseClient.callAPI("POST", `/dummy`, JSON.stringify(body), {headers, query}) } + + public async root(params: Request): Promise { + // Convert our params into the objects we need for the request + const headers = makeRecord({ + baz: params.headerBaz, + num: params.headerNum === undefined ? undefined : String(params.headerNum), + }) + + const query = makeRecord({ + bar: params.queryBar, + foo: params.queryFoo === undefined ? undefined : String(params.queryFoo), + }) + + // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) + const body: Record = { + baz: params.baz, + foo: params.foo, + } + + await this.baseClient.callAPI("POST", `/`, JSON.stringify(body), {headers, query}) + } } } diff --git a/internal/clientgen/testdata/tsapp/input.ts b/internal/clientgen/testdata/tsapp/input.ts index df15213701..c56e42c007 100644 --- a/internal/clientgen/testdata/tsapp/input.ts +++ b/internal/clientgen/testdata/tsapp/input.ts @@ -12,6 +12,11 @@ interface UnusedType { foo: Foo; } +export const root = api( + { expose: true, method: "POST", path: "/" }, + async (req: Request) => { }, +); + export const dummy = api( { expose: true, method: "POST", path: "/dummy" }, async (req: Request) => { }, diff --git a/tsparser/src/parser/respath.rs b/tsparser/src/parser/respath.rs index fcd7897535..e8346ef274 100644 --- a/tsparser/src/parser/respath.rs +++ b/tsparser/src/parser/respath.rs @@ -150,7 +150,7 @@ impl Path { // Validate the segments. for (idx, seg) in segments.iter().enumerate() { match seg { - Segment::Literal(lit) if lit.is_empty() => { + Segment::Literal(lit) if lit.is_empty() && segments.len() > 1 => { anyhow::bail!("invalid path: literal cannot be empty"); } Segment::Param { name, .. } if name.is_empty() => { @@ -215,6 +215,7 @@ mod tests { #[test] fn test_parse() { let tests = vec![ + ("/", Ok(vec![Segment::Literal("".to_string())])), ("/foo", Ok(vec![Segment::Literal("foo".to_string())])), ( "/foo/bar",