...

Source file src/go.einride.tech/aip/cmd/protoc-gen-go-aip/internal/genaip/resourcename.go

Documentation: go.einride.tech/aip/cmd/protoc-gen-go-aip/internal/genaip

     1  package genaip
     2  
     3  import (
     4  	"strconv"
     5  	"strings"
     6  
     7  	"github.com/stoewer/go-strcase"
     8  	"go.einride.tech/aip/reflect/aipreflect"
     9  	"go.einride.tech/aip/resourcename"
    10  	"google.golang.org/genproto/googleapis/api/annotations"
    11  	"google.golang.org/protobuf/compiler/protogen"
    12  	"google.golang.org/protobuf/reflect/protoregistry"
    13  )
    14  
    15  type resourceNameCodeGenerator struct {
    16  	resource *annotations.ResourceDescriptor
    17  	file     *protogen.File
    18  	files    *protoregistry.Files
    19  }
    20  
    21  func (r resourceNameCodeGenerator) GenerateCode(g *protogen.GeneratedFile) error {
    22  	if len(r.resource.GetPattern()) == 0 {
    23  		return nil
    24  	}
    25  	hasMultiPattern := len(r.resource.GetPattern()) > 1
    26  	hasFutureMultiPattern := r.resource.GetHistory() == annotations.ResourceDescriptor_FUTURE_MULTI_PATTERN
    27  	// Generate multi-pattern interface and parse methods if we have multiple patterns now or in the future.
    28  	if hasMultiPattern || hasFutureMultiPattern {
    29  		if err := r.generateMultiPatternInterface(g); err != nil {
    30  			return err
    31  		}
    32  		if err := r.generateMultiPatternParseMethod(g); err != nil {
    33  			return err
    34  		}
    35  	}
    36  
    37  	// Generate the single-pattern struct unless we explicitly only want multi-patterns from the start.
    38  	firstPattern := r.resource.GetPattern()[0]
    39  	shouldGenerateSinglePatternStruct := !hasFutureMultiPattern
    40  	firstSinglePatternStructName := r.SinglePatternStructName()
    41  	if shouldGenerateSinglePatternStruct {
    42  		if err := r.generatePatternStruct(
    43  			g, firstPattern, firstSinglePatternStructName,
    44  		); err != nil {
    45  			return err
    46  		}
    47  	}
    48  	// Generate the multi-pattern variant of the single-pattern struct if we need multi-pattern support.
    49  	// If we've already generated single-pattern structs above, ignore top-level resources here as the
    50  	// multi-pattern variant of the single-pattern struct will be identical to the single-pattern struct.
    51  	firstMultiPatternStructName := r.MultiPatternStructName(firstPattern)
    52  	equalMultiSinglePatternStructName := firstMultiPatternStructName == firstSinglePatternStructName
    53  	avoidGeneratingSameSingleStruct := shouldGenerateSinglePatternStruct && equalMultiSinglePatternStructName
    54  	if (hasMultiPattern || hasFutureMultiPattern) && !avoidGeneratingSameSingleStruct {
    55  		if err := r.generatePatternStruct(
    56  			g, firstPattern, firstMultiPatternStructName,
    57  		); err != nil {
    58  			return err
    59  		}
    60  	}
    61  	// Generate multi-pattern structs for all but the first pattern.
    62  	for _, pattern := range r.resource.GetPattern()[1:] {
    63  		if err := r.generatePatternStruct(g, pattern, r.MultiPatternStructName(pattern)); err != nil {
    64  			return err
    65  		}
    66  	}
    67  	return nil
    68  }
    69  
    70  func (r resourceNameCodeGenerator) generatePatternStruct(
    71  	g *protogen.GeneratedFile,
    72  	pattern string,
    73  	typeName string,
    74  ) error {
    75  	g.P()
    76  	g.P("type ", typeName, " struct {")
    77  	var sc resourcename.Scanner
    78  	sc.Init(pattern)
    79  	for sc.Scan() {
    80  		if sc.Segment().IsVariable() {
    81  			g.P(strcase.UpperCamelCase(string(sc.Segment().Literal())), " string")
    82  		}
    83  	}
    84  	g.P("}")
    85  	var parentConstructorsErr error
    86  	aipreflect.RangeParentResourcesInPackage(
    87  		r.files,
    88  		r.file.Desc.Package(),
    89  		pattern,
    90  		func(parent *annotations.ResourceDescriptor) bool {
    91  			if err := r.generateParentConstructorMethod(g, pattern, typeName, parent); err != nil {
    92  				parentConstructorsErr = err
    93  				return false
    94  			}
    95  			return true
    96  		},
    97  	)
    98  	if parentConstructorsErr != nil {
    99  		return parentConstructorsErr
   100  	}
   101  	if err := r.generateValidateMethod(g, pattern, typeName); err != nil {
   102  		return err
   103  	}
   104  	if err := r.generateContainsWildcardMethod(g, pattern, typeName); err != nil {
   105  		return err
   106  	}
   107  	if err := r.generateStringMethod(g, pattern, typeName); err != nil {
   108  		return err
   109  	}
   110  	if err := r.generateMarshalStringMethod(g, typeName); err != nil {
   111  		return err
   112  	}
   113  	if err := r.generateUnmarshalStringMethod(g, pattern, typeName); err != nil {
   114  		return err
   115  	}
   116  	var parentErr error
   117  	aipreflect.RangeParentResourcesInPackage(
   118  		r.files,
   119  		r.file.Desc.Package(),
   120  		pattern,
   121  		func(parent *annotations.ResourceDescriptor) bool {
   122  			if err := r.generateParentMethod(g, pattern, typeName, parent); err != nil {
   123  				parentErr = err
   124  				return false
   125  			}
   126  			return true
   127  		},
   128  	)
   129  	return parentErr
   130  }
   131  
   132  func (r *resourceNameCodeGenerator) generateParentConstructorMethod(
   133  	g *protogen.GeneratedFile,
   134  	pattern string,
   135  	typeName string,
   136  	parent *annotations.ResourceDescriptor,
   137  ) error {
   138  	var parentPattern string
   139  	for _, parentCandidate := range parent.GetPattern() {
   140  		if resourcename.HasParent(pattern, parentCandidate) {
   141  			parentPattern = parentCandidate
   142  			break
   143  		}
   144  	}
   145  	if parentPattern == "" {
   146  		return nil
   147  	}
   148  	pg := resourceNameCodeGenerator{resource: parent, file: r.file, files: r.files}
   149  	parentStruct := pg.StructName(parentPattern)
   150  	g.P()
   151  	g.P("func (n ", parentStruct, ") ", typeName, "(")
   152  	var sc resourcename.Scanner
   153  	sc.Init(strings.TrimPrefix(pattern, parentPattern))
   154  	for sc.Scan() {
   155  		if sc.Segment().IsVariable() {
   156  			g.P(strcase.LowerCamelCase(string(sc.Segment().Literal())), " string,")
   157  		}
   158  	}
   159  	g.P(") ", typeName, " {")
   160  	g.P("return ", typeName, "{")
   161  	sc.Init(parentPattern)
   162  	for sc.Scan() {
   163  		if sc.Segment().IsVariable() {
   164  			g.P(
   165  				strcase.UpperCamelCase(string(sc.Segment().Literal())),
   166  				": ",
   167  				"n.",
   168  				strcase.UpperCamelCase(string(sc.Segment().Literal())),
   169  				",",
   170  			)
   171  		}
   172  	}
   173  	sc.Init(strings.TrimPrefix(pattern, parentPattern))
   174  	for sc.Scan() {
   175  		if sc.Segment().IsVariable() {
   176  			g.P(
   177  				strcase.UpperCamelCase(string(sc.Segment().Literal())),
   178  				": ",
   179  				strcase.LowerCamelCase(string(sc.Segment().Literal())),
   180  				",",
   181  			)
   182  		}
   183  	}
   184  	g.P("}")
   185  	g.P("}")
   186  	return nil
   187  }
   188  
   189  func (r *resourceNameCodeGenerator) generateParentMethod(
   190  	g *protogen.GeneratedFile,
   191  	pattern string,
   192  	typeName string,
   193  	parent *annotations.ResourceDescriptor,
   194  ) error {
   195  	var parentPattern string
   196  	for _, parentCandidate := range parent.GetPattern() {
   197  		if resourcename.HasParent(pattern, parentCandidate) {
   198  			parentPattern = parentCandidate
   199  			break
   200  		}
   201  	}
   202  	if parentPattern == "" {
   203  		return nil
   204  	}
   205  	pg := resourceNameCodeGenerator{resource: parent, file: r.file, files: r.files}
   206  	parentStruct := pg.StructName(parentPattern)
   207  	g.P()
   208  	g.P("func (n ", typeName, ") ", parentStruct, "() ", parentStruct, " {")
   209  	g.P("return ", parentStruct, "{")
   210  	var sc resourcename.Scanner
   211  	sc.Init(parentPattern)
   212  	for sc.Scan() {
   213  		if sc.Segment().IsVariable() {
   214  			fieldName := strcase.UpperCamelCase(string(sc.Segment().Literal()))
   215  			g.P(fieldName, ": n.", fieldName, ",")
   216  		}
   217  	}
   218  	g.P("}")
   219  	g.P("}")
   220  	return nil
   221  }
   222  
   223  func (r resourceNameCodeGenerator) generateValidateMethod(
   224  	g *protogen.GeneratedFile,
   225  	pattern string,
   226  	typeName string,
   227  ) error {
   228  	stringsIndexByte := g.QualifiedGoIdent(protogen.GoIdent{
   229  		GoImportPath: "strings",
   230  		GoName:       "IndexByte",
   231  	})
   232  	fmtErrorf := g.QualifiedGoIdent(protogen.GoIdent{
   233  		GoImportPath: "fmt",
   234  		GoName:       "Errorf",
   235  	})
   236  	g.P()
   237  	g.P("func (n ", typeName, ") Validate() error {")
   238  	var sc resourcename.Scanner
   239  	sc.Init(pattern)
   240  	for sc.Scan() {
   241  		if !sc.Segment().IsVariable() {
   242  			continue
   243  		}
   244  		value := string(sc.Segment().Literal())
   245  		g.P("if n.", strcase.UpperCamelCase(value), " == ", strconv.Quote(""), "{")
   246  		g.P("return ", fmtErrorf, `("`, value, `: empty")`)
   247  		g.P("}")
   248  		g.P("if ", stringsIndexByte, "(n.", strcase.UpperCamelCase(value), ", '/') != - 1 {")
   249  		g.P("return ", fmtErrorf, `("`, value, `: contains illegal character '/'")`)
   250  		g.P("}")
   251  	}
   252  	g.P("return nil")
   253  	g.P("}")
   254  	return nil
   255  }
   256  
   257  func (r resourceNameCodeGenerator) generateContainsWildcardMethod(
   258  	g *protogen.GeneratedFile,
   259  	pattern string,
   260  	typeName string,
   261  ) error {
   262  	g.P()
   263  	g.P("func (n ", typeName, ") ContainsWildcard() bool {")
   264  	var returnStatement strings.Builder
   265  	returnStatement.WriteString("return false")
   266  	var sc resourcename.Scanner
   267  	sc.Init(pattern)
   268  	for sc.Scan() {
   269  		if !sc.Segment().IsVariable() {
   270  			continue
   271  		}
   272  		returnStatement.WriteString("|| n.")
   273  		returnStatement.WriteString(strcase.UpperCamelCase(string(sc.Segment().Literal())))
   274  		returnStatement.WriteString(" == ")
   275  		returnStatement.WriteString(strconv.Quote("-"))
   276  	}
   277  	g.P(returnStatement.String())
   278  	g.P("}")
   279  	return nil
   280  }
   281  
   282  func (r *resourceNameCodeGenerator) generateStringMethod(
   283  	g *protogen.GeneratedFile,
   284  	pattern string,
   285  	typeName string,
   286  ) error {
   287  	resourcenameSprint := g.QualifiedGoIdent(protogen.GoIdent{
   288  		GoImportPath: "go.einride.tech/aip/resourcename",
   289  		GoName:       "Sprint",
   290  	})
   291  	g.P()
   292  	g.P("func (n ", typeName, ") String() string {")
   293  	g.P("return ", resourcenameSprint, "(")
   294  	g.P(strconv.Quote(pattern), ",")
   295  	var sc resourcename.Scanner
   296  	sc.Init(pattern)
   297  	for sc.Scan() {
   298  		if sc.Segment().IsVariable() {
   299  			g.P("n.", strcase.UpperCamelCase(string(sc.Segment().Literal())), ",")
   300  		}
   301  	}
   302  	g.P(")")
   303  	g.P("}")
   304  	return nil
   305  }
   306  
   307  func (r resourceNameCodeGenerator) generateMarshalStringMethod(
   308  	g *protogen.GeneratedFile,
   309  	typeName string,
   310  ) error {
   311  	g.P()
   312  	g.P("func (n ", typeName, ") MarshalString() (string, error) {")
   313  	g.P("if err := n.Validate(); err != nil {")
   314  	g.P("return ", strconv.Quote(""), ", err")
   315  	g.P("}")
   316  	g.P("return n.String(), nil")
   317  	g.P("}")
   318  	return nil
   319  }
   320  
   321  func (r resourceNameCodeGenerator) generateUnmarshalStringMethod(
   322  	g *protogen.GeneratedFile,
   323  	pattern string,
   324  	typeName string,
   325  ) error {
   326  	resourcenameSscan := g.QualifiedGoIdent(protogen.GoIdent{
   327  		GoImportPath: "go.einride.tech/aip/resourcename",
   328  		GoName:       "Sscan",
   329  	})
   330  	g.P()
   331  	g.P("func (n *", typeName, ") UnmarshalString(name string) error {")
   332  	g.P("err := ", resourcenameSscan, "(")
   333  	g.P("name,")
   334  	g.P(strconv.Quote(pattern), ",")
   335  	var sc resourcename.Scanner
   336  	sc.Init(pattern)
   337  	for sc.Scan() {
   338  		if sc.Segment().IsVariable() {
   339  			g.P("&n.", strcase.UpperCamelCase(string(sc.Segment().Literal())), ",")
   340  		}
   341  	}
   342  	g.P(")")
   343  	g.P("if err != nil {")
   344  	g.P("return err")
   345  	g.P("}")
   346  	g.P("return n.Validate()")
   347  	g.P("}")
   348  	return nil
   349  }
   350  
   351  func (r resourceNameCodeGenerator) generateMultiPatternInterface(g *protogen.GeneratedFile) error {
   352  	fmtStringer := g.QualifiedGoIdent(protogen.GoIdent{
   353  		GoImportPath: "fmt",
   354  		GoName:       "Stringer",
   355  	})
   356  	g.P()
   357  	g.P("type ", r.MultiPatternInterfaceName(), " interface {")
   358  	g.P(fmtStringer)
   359  	g.P("MarshalString() (string, error)")
   360  	g.P("ContainsWildcard() bool")
   361  	g.P("}")
   362  	return nil
   363  }
   364  
   365  func (r *resourceNameCodeGenerator) generateMultiPatternParseMethod(g *protogen.GeneratedFile) error {
   366  	resourcenameMatch := g.QualifiedGoIdent(protogen.GoIdent{
   367  		GoImportPath: "go.einride.tech/aip/resourcename",
   368  		GoName:       "Match",
   369  	})
   370  	fmtErrorf := g.QualifiedGoIdent(protogen.GoIdent{
   371  		GoImportPath: "fmt",
   372  		GoName:       "Errorf",
   373  	})
   374  	g.P()
   375  	g.P("func Parse", r.MultiPatternInterfaceName(), "(name string) (", r.MultiPatternInterfaceName(), ", error) {")
   376  	g.P("switch {")
   377  	for _, pattern := range r.resource.GetPattern() {
   378  		g.P("case ", resourcenameMatch, "(", strconv.Quote(pattern), ", name):")
   379  		g.P("var result ", r.MultiPatternStructName(pattern))
   380  		g.P("return &result, result.UnmarshalString(name)")
   381  	}
   382  	g.P("default:")
   383  	g.P("return nil, ", fmtErrorf, `("no matching pattern")`)
   384  	g.P("}")
   385  	g.P("}")
   386  	return nil
   387  }
   388  
   389  func (r *resourceNameCodeGenerator) SinglePatternStructName() string {
   390  	return aipreflect.ResourceType(r.resource.GetType()).Type() + "ResourceName"
   391  }
   392  
   393  func (r *resourceNameCodeGenerator) StructName(pattern string) string {
   394  	if r.resource.GetHistory() == annotations.ResourceDescriptor_FUTURE_MULTI_PATTERN || len(r.resource.GetPattern()) > 1 {
   395  		return r.MultiPatternStructName(pattern)
   396  	}
   397  	if r.resource.GetPattern()[0] == pattern {
   398  		return r.SinglePatternStructName()
   399  	}
   400  	return r.MultiPatternStructName(pattern)
   401  }
   402  
   403  func (r *resourceNameCodeGenerator) MultiPatternStructName(pattern string) string {
   404  	var result strings.Builder
   405  	var sc resourcename.Scanner
   406  	sc.Init(pattern)
   407  	for sc.Scan() {
   408  		if !sc.Segment().IsVariable() && string(sc.Segment().Literal()) != r.resource.GetPlural() {
   409  			_, _ = result.WriteString(strcase.UpperCamelCase(string(sc.Segment().Literal())))
   410  		}
   411  	}
   412  	_, _ = result.WriteString(r.SinglePatternStructName())
   413  	return result.String()
   414  }
   415  
   416  func (r *resourceNameCodeGenerator) MultiPatternInterfaceName() string {
   417  	return aipreflect.ResourceType(r.resource.GetType()).Type() + "MultiPatternResourceName"
   418  }
   419  

View as plain text