From 8a2785acc571eab5e4d319e5358d38613ac6169d Mon Sep 17 00:00:00 2001 From: Lukas Jenicek Date: Fri, 18 Oct 2024 17:11:35 +0200 Subject: [PATCH] Add client and server versions of generators (webrpc, schema and ridl schema) (#72) * add client and server versions of generators (webrpc, schema and ridl schema) * rename to WebRpc * improve naming * generate only if includeHeaders option is set * rename to Webrpc * use one function to parse headers * rename all occurences to Webrpc * rename to webrpcHeader * fix conditions in main.tmpl * regenerate examples --- README.md | 1 + _examples/golang-basics/example.gen.go | 77 +++++++++++++++++++++++-- _examples/golang-basics/example.ridl | 9 ++- _examples/golang-basics/example_test.go | 8 +++ _examples/golang-basics/main.go | 24 ++++++++ _examples/golang-imports/api.gen.go | 58 +++++++++++++++++++ client.go.tmpl | 3 + main.go.tmpl | 56 ++++++++++++++++++ server.go.tmpl | 2 + 9 files changed, 232 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 76076a4..2f612c0 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Change any of the following values by passing `-option="Value"` CLI flag to `web | `-fixEmptyArrays` | `false` | force empty array `[]` instead of `null` in JSON (see Go [#27589][go27589]) | v0.13.0 | | `-errorStackTrace` | `false` | enables error stack traces | v0.14.0 | | `-legacyErrors` | `false` | enable legacy errors (v0.10.0 or older) | v0.11.0 | +| `-webrpcHeader` | `true` | enable client send webrpc version in http headers | v0.16.0 | Example: ``` diff --git a/_examples/golang-basics/example.gen.go b/_examples/golang-basics/example.gen.go index c807af7..5333931 100644 --- a/_examples/golang-basics/example.gen.go +++ b/_examples/golang-basics/example.gen.go @@ -1,4 +1,4 @@ -// example v0.0.1 05b7a5c86b98738f4fe6ce9bb1fccd4af064847a +// example v0.0.1 ccd12408e7dd53252d6e605ae1619666287d960e // -- // Code generated by webrpc-gen@v0.20.3-1-gf6584bc with ../../../gen-golang generator. DO NOT EDIT. // @@ -21,6 +21,10 @@ import ( "github.com/google/uuid" ) +const WebrpcHeader = "Webrpc" + +const WebrpcHeaderValue = "webrpc@v0.20.3-1-gf6584bc;gen-golang@unknown;example@v0.0.1" + // WebRPC description and code-gen version func WebRPCVersion() string { return "v1" @@ -33,7 +37,58 @@ func WebRPCSchemaVersion() string { // Schema hash generated from your RIDL schema func WebRPCSchemaHash() string { - return "05b7a5c86b98738f4fe6ce9bb1fccd4af064847a" + return "ccd12408e7dd53252d6e605ae1619666287d960e" +} + +type WebrpcGenVersions struct { + WebrpcGenVersion string + CodeGenName string + CodeGenVersion string + SchemaName string + SchemaVersion string +} + +func VersionFromHeader(h http.Header) (*WebrpcGenVersions, error) { + if h.Get(WebrpcHeader) == "" { + return nil, fmt.Errorf("header is empty or missing") + } + + versions, err := parseWebrpcGenVersions(h.Get(WebrpcHeader)) + if err != nil { + return nil, fmt.Errorf("webrpc header is invalid: %w", err) + } + + return versions, nil +} + +func parseWebrpcGenVersions(header string) (*WebrpcGenVersions, error) { + versions := strings.Split(header, ";") + if len(versions) < 3 { + return nil, fmt.Errorf("expected at least 3 parts while parsing webrpc header: %v", header) + } + + _, webrpcGenVersion, ok := strings.Cut(versions[0], "@") + if !ok { + return nil, fmt.Errorf("webrpc gen version could not be parsed from: %s", versions[0]) + } + + tmplTarget, tmplVersion, ok := strings.Cut(versions[1], "@") + if !ok { + return nil, fmt.Errorf("tmplTarget and tmplVersion could not be parsed from: %s", versions[1]) + } + + schemaName, schemaVersion, ok := strings.Cut(versions[2], "@") + if !ok { + return nil, fmt.Errorf("schema name and schema version could not be parsed from: %s", versions[2]) + } + + return &WebrpcGenVersions{ + WebrpcGenVersion: webrpcGenVersion, + CodeGenName: tmplTarget, + CodeGenVersion: tmplVersion, + SchemaName: schemaName, + SchemaVersion: schemaVersion, + }, nil } // @@ -143,9 +198,11 @@ type SearchFilter struct { } type Version struct { - WebrpcVersion string `json:"webrpcVersion"` - SchemaVersion string `json:"schemaVersion"` - SchemaHash string `json:"schemaHash"` + WebrpcVersion string `json:"webrpcVersion"` + SchemaVersion string `json:"schemaVersion"` + SchemaHash string `json:"schemaHash"` + ClientGenVersion *GenVersions `json:"clientGenVersion"` + ServerGenVersion *GenVersions `json:"serverGenVersion"` } type ComplexType struct { @@ -160,6 +217,13 @@ type ComplexType struct { User *User `json:"user"` } +type GenVersions struct { + WebrpcGenVersion string `json:"WebrpcGenVersion"` + TmplTarget string `json:"TmplTarget"` + TmplVersion string `json:"TmplVersion"` + SchemaVersion string `json:"SchemaVersion"` +} + var ( methods = map[string]method{ "/rpc/ExampleService/Ping": { @@ -267,6 +331,8 @@ func (s *exampleServiceServer) ServeHTTP(w http.ResponseWriter, r *http.Request) } }() + w.Header().Set(WebrpcHeader, WebrpcHeaderValue) + ctx := r.Context() ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w) ctx = context.WithValue(ctx, HTTPRequestCtxKey, r) @@ -707,6 +773,7 @@ func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType } req.Header.Set("Accept", contentType) req.Header.Set("Content-Type", contentType) + req.Header.Set(WebrpcHeader, WebrpcHeaderValue) if headers, ok := HTTPRequestHeaders(ctx); ok { for k := range headers { for _, v := range headers[k] { diff --git a/_examples/golang-basics/example.ridl b/_examples/golang-basics/example.ridl index d60169f..87c0da2 100644 --- a/_examples/golang-basics/example.ridl +++ b/_examples/golang-basics/example.ridl @@ -2,7 +2,6 @@ webrpc = v1 # version of webrpc schema format (ridl or json) name = example # name of your backend app version = v0.0.1 # version of your schema - # user role # which defines which type of operations user can do @@ -69,6 +68,8 @@ struct Version - webrpcVersion: string - schemaVersion: string - schemaHash: string + - clientGenVersion: GenVersions + - serverGenVersion: GenVersions struct ComplexType - meta: map @@ -81,6 +82,12 @@ struct ComplexType - mapOfUsers: map - user: User +struct GenVersions + - WebrpcGenVersion: string + - TmplTarget: string + - TmplVersion: string + - SchemaVersion: string + error 500100 MissingArgument "missing argument" error 500101 InvalidUsername "invalid username" error 400100 MemoryFull "system memory is full" diff --git a/_examples/golang-basics/example_test.go b/_examples/golang-basics/example_test.go index 1da0f52..7340a19 100644 --- a/_examples/golang-basics/example_test.go +++ b/_examples/golang-basics/example_test.go @@ -38,6 +38,14 @@ func TestStatus(t *testing.T) { assert.NoError(t, err) } +func TestVersion(t *testing.T) { + version, err := client.Version(context.Background()) + + assert.NoError(t, err) + assert.NotNil(t, version.ClientGenVersion) + assert.NotNil(t, version.ServerGenVersion) +} + func TestGetUser(t *testing.T) { { arg1 := map[string]string{"a": "1"} diff --git a/_examples/golang-basics/main.go b/_examples/golang-basics/main.go index 61b5a50..637f49f 100644 --- a/_examples/golang-basics/main.go +++ b/_examples/golang-basics/main.go @@ -48,10 +48,34 @@ func (rpc *ExampleServiceRPC) Status(ctx context.Context) (bool, error) { } func (rpc *ExampleServiceRPC) Version(ctx context.Context) (*Version, error) { + resp := ResponseWriterFromContext(ctx) + serverVersions, err := VersionFromHeader(resp.Header()) + if err != nil { + return nil, fmt.Errorf("parse server webrpc gen versions: %w", err) + } + + req := RequestFromContext(ctx) + clientVersions, err := VersionFromHeader(req.Header) + if err != nil { + return nil, fmt.Errorf("parse client webrpc gen versions: %w", err) + } + return &Version{ WebrpcVersion: WebRPCVersion(), SchemaVersion: WebRPCSchemaVersion(), SchemaHash: WebRPCSchemaHash(), + ClientGenVersion: &GenVersions{ + WebrpcGenVersion: clientVersions.WebrpcGenVersion, + TmplTarget: clientVersions.CodeGenName, + TmplVersion: clientVersions.CodeGenVersion, + SchemaVersion: clientVersions.CodeGenVersion, + }, + ServerGenVersion: &GenVersions{ + WebrpcGenVersion: serverVersions.WebrpcGenVersion, + TmplTarget: serverVersions.CodeGenName, + TmplVersion: serverVersions.CodeGenVersion, + SchemaVersion: serverVersions.CodeGenVersion, + }, }, nil } diff --git a/_examples/golang-imports/api.gen.go b/_examples/golang-imports/api.gen.go index 4b58c54..6e16407 100644 --- a/_examples/golang-imports/api.gen.go +++ b/_examples/golang-imports/api.gen.go @@ -18,6 +18,10 @@ import ( ) +const WebrpcHeader = "Webrpc" + +const WebrpcHeaderValue = "webrpc@v0.20.3-1-gf6584bc;gen-golang@unknown;example-api-service@v1.0.0" + // WebRPC description and code-gen version func WebRPCVersion() string { return "v1" @@ -33,6 +37,57 @@ func WebRPCSchemaHash() string { return "cae4e128f4fb4c938bfe1ea312deeea3dfd6b6af" } +type WebrpcGenVersions struct { + WebrpcGenVersion string + CodeGenName string + CodeGenVersion string + SchemaName string + SchemaVersion string +} + +func VersionFromHeader(h http.Header) (*WebrpcGenVersions, error) { + if h.Get(WebrpcHeader) == "" { + return nil, fmt.Errorf("header is empty or missing") + } + + versions, err := parseWebrpcGenVersions(h.Get(WebrpcHeader)) + if err != nil { + return nil, fmt.Errorf("webrpc header is invalid: %w", err) + } + + return versions, nil +} + +func parseWebrpcGenVersions(header string) (*WebrpcGenVersions, error) { + versions := strings.Split(header, ";") + if len(versions) < 3 { + return nil, fmt.Errorf("expected at least 3 parts while parsing webrpc header: %v", header) + } + + _, webrpcGenVersion, ok := strings.Cut(versions[0], "@") + if !ok { + return nil, fmt.Errorf("webrpc gen version could not be parsed from: %s", versions[0]) + } + + tmplTarget, tmplVersion, ok := strings.Cut(versions[1], "@") + if !ok { + return nil, fmt.Errorf("tmplTarget and tmplVersion could not be parsed from: %s", versions[1]) + } + + schemaName, schemaVersion, ok := strings.Cut(versions[2], "@") + if !ok { + return nil, fmt.Errorf("schema name and schema version could not be parsed from: %s", versions[2]) + } + + return &WebrpcGenVersions{ + WebrpcGenVersion: webrpcGenVersion, + CodeGenName: tmplTarget, + CodeGenVersion: tmplVersion, + SchemaName: schemaName, + SchemaVersion: schemaVersion, + }, nil +} + // // Common types // @@ -167,6 +222,8 @@ func (s *exampleAPIServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { } }() + w.Header().Set(WebrpcHeader, WebrpcHeaderValue) + ctx := r.Context() ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w) ctx = context.WithValue(ctx, HTTPRequestCtxKey, r) @@ -433,6 +490,7 @@ func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType } req.Header.Set("Accept", contentType) req.Header.Set("Content-Type", contentType) + req.Header.Set(WebrpcHeader, WebrpcHeaderValue) if headers, ok := HTTPRequestHeaders(ctx); ok { for k := range headers { for _, v := range headers[k] { diff --git a/client.go.tmpl b/client.go.tmpl index 6d1be1d..cf8a278 100644 --- a/client.go.tmpl +++ b/client.go.tmpl @@ -208,6 +208,9 @@ func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType } req.Header.Set("Accept", contentType) req.Header.Set("Content-Type", contentType) + {{- if eq $opts.webrpcHeader true }} + req.Header.Set(WebrpcHeader, WebrpcHeaderValue) + {{- end }} if headers, ok := HTTPRequestHeaders(ctx); ok { for k := range headers { for _, v := range headers[k] { diff --git a/main.go.tmpl b/main.go.tmpl index e298dd4..4685ec5 100644 --- a/main.go.tmpl +++ b/main.go.tmpl @@ -11,6 +11,7 @@ {{- set $opts "fixEmptyArrays" (ternary (in .Opts.fixEmptyArrays "" "true") true false) -}} {{- set $opts "errorStackTrace" (ternary (in .Opts.errorStackTrace "" "true") true false) -}} {{- set $opts "legacyErrors" (ternary (in .Opts.legacyErrors "" "true") true false) -}} +{{- set $opts "webrpcHeader" (ternary (eq (default .Opts.webrpcHeader "true") "false") false true) -}} {{- $typePrefix := (last (split "/" $opts.importTypesFrom)) -}} {{- if ne $typePrefix "" -}} @@ -81,6 +82,10 @@ package {{get $opts "pkg"}} {{template "imports" dict "Types" .Types "Opts" $opts }} +const WebrpcHeader = "Webrpc" + +const WebrpcHeaderValue = "{{ .WebrpcHeader }}" + // WebRPC description and code-gen version func WebRPCVersion() string { return "{{.WebrpcVersion}}" @@ -96,6 +101,57 @@ func WebRPCSchemaHash() string { return "{{.SchemaHash}}" } +type WebrpcGenVersions struct { + WebrpcGenVersion string + CodeGenName string + CodeGenVersion string + SchemaName string + SchemaVersion string +} + +func VersionFromHeader(h http.Header) (*WebrpcGenVersions, error) { + if h.Get(WebrpcHeader) == "" { + return nil, fmt.Errorf("header is empty or missing") + } + + versions, err := parseWebrpcGenVersions(h.Get(WebrpcHeader)) + if err != nil { + return nil, fmt.Errorf("webrpc header is invalid: %w", err) + } + + return versions, nil +} + +func parseWebrpcGenVersions(header string) (*WebrpcGenVersions, error) { + versions := strings.Split(header, ";") + if len(versions) < 3 { + return nil, fmt.Errorf("expected at least 3 parts while parsing webrpc header: %v", header) + } + + _, webrpcGenVersion, ok := strings.Cut(versions[0], "@") + if !ok { + return nil, fmt.Errorf("webrpc gen version could not be parsed from: %s", versions[0]) + } + + tmplTarget, tmplVersion, ok := strings.Cut(versions[1], "@") + if !ok { + return nil, fmt.Errorf("tmplTarget and tmplVersion could not be parsed from: %s", versions[1]) + } + + schemaName, schemaVersion, ok := strings.Cut(versions[2], "@") + if !ok { + return nil, fmt.Errorf("schema name and schema version could not be parsed from: %s", versions[2]) + } + + return &WebrpcGenVersions{ + WebrpcGenVersion: webrpcGenVersion, + CodeGenName: tmplTarget, + CodeGenVersion: tmplVersion, + SchemaName: schemaName, + SchemaVersion: schemaVersion, + }, nil +} + {{- printf "\n" -}} {{- if eq $opts.importTypesFrom "" }} diff --git a/server.go.tmpl b/server.go.tmpl index 49f9751..b2742d4 100644 --- a/server.go.tmpl +++ b/server.go.tmpl @@ -38,6 +38,8 @@ func (s *{{$serviceName}}) ServeHTTP(w http.ResponseWriter, r *http.Request) { } }() + w.Header().Set(WebrpcHeader, WebrpcHeaderValue) + ctx := r.Context() ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w) ctx = context.WithValue(ctx, HTTPRequestCtxKey, r)