Skip to content

Commit

Permalink
Ensure consistent camel casing with protobuf library, fixes #1 (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
dpup authored May 11, 2024
1 parent 9479ad2 commit 6341565
Show file tree
Hide file tree
Showing 11 changed files with 406 additions and 147 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ testdata:
protoc -I ./test/testdata/. \
--grpc-gateway-ts_out ./test/testdata/ \
--grpc-gateway-ts_opt logtostderr=true \
log.proto environment.proto datasource/datasource.proto
log.proto environment.proto names.proto datasource/datasource.proto

.PHONY: lint
lint:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
2. Supports both one way and server side streaming gRPC calls.
3. POJO request construction guarded by message type definitions, which is way easier compare to `grpc-web`.
4. No need to use swagger/open api to generate client code for the web.
5. Fixes inconsistent field naming when fields contain numbers, e.g. `k8s_field` --> `k8sField`.

### Changes made since the fork

Expand Down
25 changes: 25 additions & 0 deletions generator/strings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package generator

// JSONCamelCase converts a snake_case identifier to a camelCase identifier,
// according to the protobuf JSON specification.
//
// Copied from: google.golang.org/protobuf/internal/strs
func JSONCamelCase(s string) string {
var b []byte
var wasUnderscore bool
for i := 0; i < len(s); i++ { // proto identifiers are always ASCII
c := s[i]
if c != '_' {
if wasUnderscore && isASCIILower(c) {
c -= 'a' - 'A' // convert to uppercase
}
b = append(b, c)
}
wasUnderscore = c == '_'
}
return string(b)
}

func isASCIILower(c byte) bool {
return 'a' <= c && c <= 'z'
}
3 changes: 1 addition & 2 deletions generator/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
log "github.com/sirupsen/logrus"

"github.com/Masterminds/sprig"
"github.com/iancoleman/strcase"

"github.com/dpup/protoc-gen-grpc-gateway-ts/data"
"github.com/dpup/protoc-gen-grpc-gateway-ts/registry"
Expand Down Expand Up @@ -151,7 +150,7 @@ func fieldName(r *registry.Registry) func(name string) string {
if r.UseProtoNames {
return name
}
return strcase.ToLowerCamel(name)
return JSONCamelCase(name)
}
}

Expand Down
37 changes: 37 additions & 0 deletions generator/template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package generator

import (
"testing"

"github.com/dpup/protoc-gen-grpc-gateway-ts/registry"
"github.com/stretchr/testify/assert"
)

func TestFieldName(t *testing.T) {
tests := []struct {
useProtoNames bool
input string
want string
}{
{useProtoNames: false, input: "k8s_field", want: "k8sField"},

{useProtoNames: false, input: "foo_bar", want: "fooBar"},
{useProtoNames: false, input: "foobar", want: "foobar"},
{useProtoNames: false, input: "foo_bar_baz", want: "fooBarBaz"},

{useProtoNames: false, input: "foobar3", want: "foobar3"},
{useProtoNames: false, input: "foo3bar", want: "foo3bar"},
{useProtoNames: false, input: "foo3_bar", want: "foo3Bar"},
{useProtoNames: false, input: "foo_3bar", want: "foo3bar"},
{useProtoNames: false, input: "foo_3_bar", want: "foo3Bar"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
r := &registry.Registry{UseProtoNames: tt.useProtoNames}
fn := fieldName(r)
if got := fn(tt.input); got != tt.want {
assert.Equal(t, got, tt.want, "fieldName(%s) = %s, want %s", tt.input, got, tt.want)
}
})
}
}
303 changes: 217 additions & 86 deletions test/integration/service.pb.go

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions test/integration/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,17 @@ message OptionalFieldsSubMsg {
optional string opt_str = 2;
}

message Names {
string foo_bar = 1;
string bazbam = 2;
string binbom3 = 3;
string tin3tam = 4;
string ting3_tang = 5;
string king_3kong = 6;
string frim_3_fram = 7;
string k8s_field = 8;
}

service CounterService {
rpc Increment(UnaryRequest) returns (UnaryResponse);
rpc StreamingIncrements(StreamingRequest) returns (stream StreamingResponse);
Expand Down
26 changes: 26 additions & 0 deletions test/names_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package test

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestFieldsWithNumbers(t *testing.T) {
// Verifies that the proto generates the field name in the right format.

createTestFile("fieldsWithNumbers.ts", `
import {FieldsWithNumbers} from "./names.pb"
export const newFieldsWithNumbers = (f: FieldsWithNumbers) => f.k8sField;
export const result = newFieldsWithNumbers({k8sField: 'test'});
`)

defer removeTestFile("fieldsWithNumbers.ts")

result := runTsc()
assert.Nil(t, result.err)
assert.Equal(t, 0, result.exitCode)

assert.Empty(t, result.stderr)
assert.Empty(t, result.stdout)
}
75 changes: 17 additions & 58 deletions test/oneof_test.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
package test

import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)

var projectRoot = ""

func init() {
wd, _ := os.Getwd()
for !strings.HasSuffix(wd, "protoc-gen-grpc-gateway-ts") {
wd = filepath.Dir(wd)
}
projectRoot = wd
}

func TestValidOneOfUseCase(t *testing.T) {
f, err := createFileWithContent("valid.ts", `
createTestFile("valid.ts", `
import {LogEntryLevel, LogService} from "./log.pb";
import {DataSource} from "./datasource/datasource.pb"
import {Environment} from "./environment.pb"
Expand Down Expand Up @@ -64,64 +49,38 @@ import {Environment} from "./environment.pb"
})
})()
`)
assert.Nil(t, err)
defer f.Close()
cmd := getTSCCommand()
err = cmd.Run()
assert.Nil(t, err)
assert.Equal(t, 0, cmd.ProcessState.ExitCode())

err = removeTestFile("valid.ts")
assert.Nil(t, err)
}
defer removeTestFile("valid.ts")

result := runTsc()
assert.Nil(t, result.err)
assert.Equal(t, 0, result.exitCode)

func getTSCCommand() *exec.Cmd {
cmd := exec.Command("npx", "tsc", "--project", ".", "--noEmit")
cmd.Dir = projectRoot + "/test/testdata/"
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
return cmd
assert.Empty(t, result.stderr)
assert.Empty(t, result.stdout)
}

func TestInvalidOneOfUseCase(t *testing.T) {
f, err := createFileWithContent("invalid.ts", `
createTestFile("invalid.ts", `
import {LogService} from "./log.pb";
import {DataSource} from "./datasource/datasource.pb"
(async () => {
const cloudSourceResult = await LogService.FetchLog({
source: DataSource.Cloud,
service: "cloudService",
application: "cloudApplication"
application: "cloudApplication",
})
})()
`)
assert.Nil(t, err)
defer f.Close()
cmd := getTSCCommand()
err = cmd.Run()
assert.NotNil(t, err)
assert.NotEqual(t, 0, cmd.ProcessState.ExitCode())
defer removeTestFile("invalid.ts")

err = removeTestFile("invalid.ts")
assert.Nil(t, err)
}

func createFileWithContent(fname, content string) (*os.File, error) {
f, err := os.Create(projectRoot + "/test/testdata/" + fname)
if err != nil {
return nil, errors.Wrapf(err, "error creating file")
}
defer f.Close()
_, err = f.WriteString(content)
if err != nil {
return nil, errors.Wrapf(err, "error writing content into %s", fname)
}

return f, nil
}
result := runTsc()
assert.NotNil(t, result.err)
assert.NotEqual(t, 0, result.exitCode)

func removeTestFile(fname string) error {
return os.Remove(projectRoot + "/test/testdata/" + fname)
assert.Contains(t, result.stderr,
"Argument of type '{ source: DataSource.Cloud; service: string; application: "+
"string; }' is not assignable to parameter of type 'FetchLogRequest'.")
}
62 changes: 62 additions & 0 deletions test/test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package test

import (
"bytes"
"os"
"os/exec"
"path/filepath"
"strings"
)

var projectRoot = ""

func init() {
wd, _ := os.Getwd()
for !strings.HasSuffix(wd, "protoc-gen-grpc-gateway-ts") {
wd = filepath.Dir(wd)
}
projectRoot = wd
}

func runTsc() cmdResult {
cmd := exec.Command("npx", "tsc", "--project", ".", "--noEmit")
cmd.Dir = projectRoot + "/test/testdata/"

cmdOutput := new(bytes.Buffer)
cmdError := new(bytes.Buffer)
cmd.Stderr = cmdOutput
cmd.Stdout = cmdError

err := cmd.Run()

return cmdResult{
stdout: cmdOutput.String(),
stderr: cmdError.String(),
err: err,
exitCode: cmd.ProcessState.ExitCode(),
}
}

type cmdResult struct {
stdout string
stderr string
err error
exitCode int
}

func createTestFile(fname, content string) {
f, err := os.Create(projectRoot + "/test/testdata/" + fname)
if err != nil {
panic(err)
}
defer f.Close()
if _, err = f.WriteString(content); err != nil {
panic(err)
}
}

func removeTestFile(fname string) {
if err := os.Remove(projectRoot + "/test/testdata/" + fname); err != nil {
panic(err)
}
}
8 changes: 8 additions & 0 deletions test/testdata/names.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
syntax = "proto3";

package com.squareup.cash.gap;

// See https://github.com/dpup/protoc-gen-grpc-gateway-ts/issues/1
message FieldsWithNumbers {
string k8s_field = 1;
}

0 comments on commit 6341565

Please sign in to comment.