Skip to content

Commit

Permalink
Naming strategy (grpc-ecosystem#2310)
Browse files Browse the repository at this point in the history
* naming-strategy start

* main_test change

* main_test make it more readable

* bazel build

* Regenerate files

Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
  • Loading branch information
alperengozeten and johanbrandhorst authored Sep 3, 2021
1 parent 9185677 commit e257a35
Show file tree
Hide file tree
Showing 9 changed files with 435 additions and 146 deletions.
43 changes: 30 additions & 13 deletions internal/descriptor/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,15 @@ type Registry struct {
// with gRPC-Gateway response, if it uses json tags for marshaling.
useJSONNamesForFields bool

// useFQNForOpenAPIName if true OpenAPI names will use the full qualified name (FQN) from proto definition,
// and generate a dot-separated OpenAPI name concatenating all elements from the proto FQN.
// If false, the default behavior is to concat the last 2 elements of the FQN if they are unique, otherwise concat
// all the elements of the FQN without any separator
useFQNForOpenAPIName bool
// openAPINamingStrategy is the naming strategy to use for assigning OpenAPI field and parameter names. This can be one of the following:
// - `legacy`: use the legacy naming strategy from protoc-gen-swagger, that generates unique but not necessarily
// maximally concise names. Components are concatenated directly, e.g., `MyOuterMessageMyNestedMessage`.
// - `simple`: use a simple heuristic for generating unique and concise names. Components are concatenated using
// dots as a separator, e.g., `MyOuterMesage.MyNestedMessage` (if `MyNestedMessage` alone is unique,
// `MyNestedMessage` will be used as the OpenAPI name).
// - `fqn`: always use the fully-qualified name of the proto message (leading dot removed) as the OpenAPI
// name.
openAPINamingStrategy string

// useGoTemplate determines whether you want to use GO templates
// in your protofile comments
Expand Down Expand Up @@ -133,12 +137,13 @@ type annotationIdentifier struct {
// NewRegistry returns a new Registry.
func NewRegistry() *Registry {
return &Registry{
msgs: make(map[string]*Message),
enums: make(map[string]*Enum),
files: make(map[string]*File),
pkgMap: make(map[string]string),
pkgAliases: make(map[string]string),
externalHTTPRules: make(map[string][]*annotations.HttpRule),
msgs: make(map[string]*Message),
enums: make(map[string]*Enum),
files: make(map[string]*File),
pkgMap: make(map[string]string),
pkgAliases: make(map[string]string),
externalHTTPRules: make(map[string][]*annotations.HttpRule),
openAPINamingStrategy: "legacy",
repeatedPathParamSeparator: repeatedFieldSeparator{
name: "csv",
sep: ',',
Expand Down Expand Up @@ -506,20 +511,32 @@ func (r *Registry) GetUseJSONNamesForFields() bool {
}

// SetUseFQNForOpenAPIName sets useFQNForOpenAPIName
// Deprecated: use SetOpenAPINamingStrategy instead.
func (r *Registry) SetUseFQNForOpenAPIName(use bool) {
r.useFQNForOpenAPIName = use
r.openAPINamingStrategy = "fqn"
}

// GetUseFQNForOpenAPIName returns useFQNForOpenAPIName
// Deprecated: Use GetOpenAPINamingStrategy().
func (r *Registry) GetUseFQNForOpenAPIName() bool {
return r.useFQNForOpenAPIName
return r.openAPINamingStrategy == "fqn"
}

// GetMergeFileName return the target merge OpenAPI file name
func (r *Registry) GetMergeFileName() string {
return r.mergeFileName
}

// SetOpenAPINamingStrategy sets the naming strategy to be used.
func (r *Registry) SetOpenAPINamingStrategy(strategy string) {
r.openAPINamingStrategy = strategy
}

// GetOpenAPINamingStrategy retrieves the naming strategy that is in use.
func (r *Registry) GetOpenAPINamingStrategy() string {
return r.openAPINamingStrategy
}

// SetUseGoTemplate sets useGoTemplate
func (r *Registry) SetUseGoTemplate(use bool) {
r.useGoTemplate = use
Expand Down
14 changes: 14 additions & 0 deletions protoc-gen-openapiv2/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def _run_proto_gen_openapi(
repeated_path_param_separator,
include_package_in_tags,
fqn_for_openapi_name,
openapi_naming_strategy,
use_go_templates,
disable_default_errors,
enums_as_ints,
Expand Down Expand Up @@ -87,6 +88,9 @@ def _run_proto_gen_openapi(
if fqn_for_openapi_name:
args.add("--openapiv2_opt", "fqn_for_openapi_name=true")

if openapi_naming_strategy:
args.add("--openapiv2_opt", "openapi_naming_strategy=%s" % openapi_naming_strategy)

if generate_unbound_methods:
args.add("--openapiv2_opt", "generate_unbound_methods=true")

Expand Down Expand Up @@ -201,6 +205,7 @@ def _proto_gen_openapi_impl(ctx):
repeated_path_param_separator = ctx.attr.repeated_path_param_separator,
include_package_in_tags = ctx.attr.include_package_in_tags,
fqn_for_openapi_name = ctx.attr.fqn_for_openapi_name,
openapi_naming_strategy = ctx.attr.openapi_naming_strategy,
use_go_templates = ctx.attr.use_go_templates,
disable_default_errors = ctx.attr.disable_default_errors,
enums_as_ints = ctx.attr.enums_as_ints,
Expand Down Expand Up @@ -261,6 +266,15 @@ protoc_gen_openapiv2 = rule(
" qualified names from the proto definition" +
" (ie my.package.MyMessage.MyInnerMessage",
),
"openapi_naming_strategy": attr.string(
default = "",
mandatory = False,
values = ["", "simple", "legacy", "fqn"],
doc = "configures how OpenAPI names are determined." +
" Allowed values are `` (empty), `simple`, `legacy` and `fqn`." +
" If unset, either `legacy` or `fqn` are selected, depending" +
" on the value of the `fqn_for_openapi_name` setting",
),
"use_go_templates": attr.bool(
default = False,
mandatory = False,
Expand Down
2 changes: 2 additions & 0 deletions protoc-gen-openapiv2/internal/genopenapi/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ go_library(
"generator.go",
"helpers.go",
"helpers_go111_old.go",
"naming.go",
"template.go",
"types.go",
],
Expand Down Expand Up @@ -36,6 +37,7 @@ go_test(
size = "small",
srcs = [
"cycle_test.go",
"naming_test.go",
"template_test.go",
],
embed = [":genopenapi"],
Expand Down
110 changes: 110 additions & 0 deletions protoc-gen-openapiv2/internal/genopenapi/naming.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package genopenapi

import (
"reflect"
"strings"
)

// LookupNamingStrategy looks up the given naming strategy and returns the naming
// strategy function for it. The naming strategy function takes in the list of all
// fully-qualified proto message names, and returns a mapping from fully-qualified
// name to OpenAPI name.
func LookupNamingStrategy(strategyName string) func([]string) map[string]string {
switch strings.ToLower(strategyName) {
case "fqn":
return resolveNamesFQN
case "legacy":
return resolveNamesLegacy
case "simple":
return resolveNamesSimple
}
return nil
}

// resolveNamesFQN uses the fully-qualified proto message name as the
// OpenAPI name, stripping the leading dot.
func resolveNamesFQN(messages []string) map[string]string {
uniqueNames := make(map[string]string, len(messages))
for _, p := range messages {
// strip leading dot from proto fqn
uniqueNames[p] = p[1:]
}
return uniqueNames
}

// resolveNamesLegacy takes the names of all proto messages and generates unique references by
// applying the legacy heuristics for deriving unique names: starting from the bottom of the name hierarchy, it
// determines the minimum number of components necessary to yield a unique name, adds one
// to that number, and then concatenates those last components with no separator in between
// to form a unique name.
//
// E.g., if the fully qualified name is `.a.b.C.D`, and there are other messages with fully
// qualified names ending in `.D` but not in `.C.D`, it assigns the unique name `bCD`.
func resolveNamesLegacy(messages []string) map[string]string {
return resolveNamesUniqueWithContext(messages, 1, "")
}

// resolveNamesSimple takes the names of all proto messages and generates unique references by using a simple
// heuristic: starting from the bottom of the name hierarchy, it determines the minimum
// number of components necessary to yield a unique name, and then concatenates those last
// components with a "." separator in between to form a unique name.
//
// E.g., if the fully qualified name is `.a.b.C.D`, and there are other messages with
// fully qualified names ending in `.D` but not in `.C.D`, it assigns the unique name `C.D`.
func resolveNamesSimple(messages []string) map[string]string {
return resolveNamesUniqueWithContext(messages, 0, ".")
}

// Take the names of every proto message and generates a unique reference by:
// first, separating each message name into its components by splitting at dots. Then,
// take the shortest suffix slice from each components slice that is unique among all
// messages, and convert it into a component name by taking extraContext additional
// components into consideration and joining all components with componentSeparator.
func resolveNamesUniqueWithContext(messages []string, extraContext int, componentSeparator string) map[string]string {
packagesByDepth := make(map[int][][]string)
uniqueNames := make(map[string]string)

hierarchy := func(pkg string) []string {
return strings.Split(pkg, ".")
}

for _, p := range messages {
h := hierarchy(p)
for depth := range h {
if _, ok := packagesByDepth[depth]; !ok {
packagesByDepth[depth] = make([][]string, 0)
}
packagesByDepth[depth] = append(packagesByDepth[depth], h[len(h)-depth:])
}
}

count := func(list [][]string, item []string) int {
i := 0
for _, element := range list {
if reflect.DeepEqual(element, item) {
i++
}
}
return i
}

for _, p := range messages {
h := hierarchy(p)
depth := 0
for ; depth < len(h); depth++ {
// depth + extraContext > 0 ensures that we only break for values of depth when the
// resulting slice of name components is non-empty. Otherwise, we would return the
// empty string as the concise unique name is len(messages) == 1 (which is
// technically correct).
if depth+extraContext > 0 && count(packagesByDepth[depth], h[len(h)-depth:]) == 1 {
break
}
}
start := len(h) - depth - extraContext
if start < 0 {
start = 0
}
uniqueNames[p] = strings.Join(h[start:], componentSeparator)
}
return uniqueNames
}
53 changes: 53 additions & 0 deletions protoc-gen-openapiv2/internal/genopenapi/naming_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package genopenapi

import "testing"

func TestNaming(t *testing.T) {
type expectedNames struct {
fqn, legacy, simple string
}
messageNameToExpected := map[string]expectedNames{
".A": {"A", "A", "A"},
".a.B.C": {"a.B.C", "aBC", "B.C"},
".a.D.C": {"a.D.C", "aDC", "D.C"},
".a.E.F": {"a.E.F", "aEF", "a.E.F"},
".b.E.F": {"b.E.F", "bEF", "b.E.F"},
".c.G.H": {"c.G.H", "GH", "H"},
}

allMessageNames := make([]string, 0, len(messageNameToExpected))
for msgName := range messageNameToExpected {
allMessageNames = append(allMessageNames, msgName)
}

t.Run("fqn", func(t *testing.T) {
uniqueNames := resolveNamesFQN(allMessageNames)
for _, msgName := range allMessageNames {
expected := messageNameToExpected[msgName].fqn
actual := uniqueNames[msgName]
if expected != actual {
t.Errorf("fqn unique name %q does not match expected name %q", actual, expected)
}
}
})
t.Run("legacy", func(t *testing.T) {
uniqueNames := resolveNamesLegacy(allMessageNames)
for _, msgName := range allMessageNames {
expected := messageNameToExpected[msgName].legacy
actual := uniqueNames[msgName]
if expected != actual {
t.Errorf("legacy unique name %q does not match expected name %q", actual, expected)
}
}
})
t.Run("simple", func(t *testing.T) {
uniqueNames := resolveNamesSimple(allMessageNames)
for _, msgName := range allMessageNames {
expected := messageNameToExpected[msgName].simple
actual := uniqueNames[msgName]
if expected != actual {
t.Errorf("simple unique name %q does not match expected name %q", actual, expected)
}
}
})
}
60 changes: 7 additions & 53 deletions protoc-gen-openapiv2/internal/genopenapi/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,7 @@ func fullyQualifiedNameToOpenAPIName(fqn string, reg *descriptor.Registry) (stri
ret, ok := mapping[fqn]
return ret, ok
}
mapping := resolveFullyQualifiedNameToOpenAPINames(append(reg.GetAllFQMNs(), reg.GetAllFQENs()...), reg.GetUseFQNForOpenAPIName())
mapping := resolveFullyQualifiedNameToOpenAPINames(append(reg.GetAllFQMNs(), reg.GetAllFQENs()...), reg.GetOpenAPINamingStrategy())
registriesSeen[reg] = mapping
ret, ok := mapping[fqn]
return ret, ok
Expand All @@ -738,59 +738,13 @@ func lookupMsgAndOpenAPIName(location, name string, reg *descriptor.Registry) (*
var registriesSeen = map[*descriptor.Registry]map[string]string{}
var registriesSeenMutex sync.Mutex

// Take the names of every proto and "uniq-ify" them. The idea is to produce a
// set of names that meet a couple of conditions. They must be stable, they
// must be unique, and they must be shorter than the FQN.
//
// This likely could be made better. This will always generate the same names
// but may not always produce optimal names. This is a reasonably close
// approximation of what they should look like in most cases.
func resolveFullyQualifiedNameToOpenAPINames(messages []string, useFQNForOpenAPIName bool) map[string]string {
packagesByDepth := make(map[int][][]string)
uniqueNames := make(map[string]string)

hierarchy := func(pkg string) []string {
return strings.Split(pkg, ".")
}

for _, p := range messages {
h := hierarchy(p)
for depth := range h {
if _, ok := packagesByDepth[depth]; !ok {
packagesByDepth[depth] = make([][]string, 0)
}
packagesByDepth[depth] = append(packagesByDepth[depth], h[len(h)-depth:])
}
}

count := func(list [][]string, item []string) int {
i := 0
for _, element := range list {
if reflect.DeepEqual(element, item) {
i++
}
}
return i
}

for _, p := range messages {
if useFQNForOpenAPIName {
// strip leading dot from proto fqn
uniqueNames[p] = p[1:]
} else {
h := hierarchy(p)
for depth := 0; depth < len(h); depth++ {
if count(packagesByDepth[depth], h[len(h)-depth:]) == 1 {
uniqueNames[p] = strings.Join(h[len(h)-depth-1:], "")
break
}
if depth == len(h)-1 {
uniqueNames[p] = strings.Join(h, "")
}
}
}
// Take the names of every proto message and generate a unique reference for each, according to the given strategy.
func resolveFullyQualifiedNameToOpenAPINames(messages []string, namingStrategy string) map[string]string {
strategyFn := LookupNamingStrategy(namingStrategy)
if strategyFn == nil {
return nil
}
return uniqueNames
return strategyFn(messages)
}

var canRegexp = regexp.MustCompile("{([a-zA-Z][a-zA-Z0-9_.]*).*}")
Expand Down
Loading

0 comments on commit e257a35

Please sign in to comment.