Categorygithub.com/ucarion/protoc-gen-go-resource

# README

protoc-gen-go-resource

protoc-gen-go-resource is a protoc plugin that generates convenience functions for Protobuf messages annotated with google.api.resource .

For example, this example message, taken from Google's AIP 123 ("Resource Names"):

syntax = "proto3";

import "google/api/resource.proto";

// A representation of a Pub/Sub topic.
message Topic {
  option (google.api.resource) = {
    type: "pubsub.googleapis.com/Topic"
    pattern: "projects/{project}/topics/{topic}"
  };

  // The resource name of the topic.
  string name = 1;

  // Other fields...
}

Would have, in addition to the types usually generated for Topic, the following new functionality:

package pubsub

type ParsedTopicName struct {
	ProjectID string
	TopicID   string
}

func ParseTopicName(s string) (ParsedTopicName, error) {
	// Parses names like "projects/foo/topics/bar" ...
}

func ParseFullTopicName(s string) (ParsedTopicName, error) {
	// Parses names like "//pubsub.googleapis.com/projects/foo/topics/bar" ...
}

func (n ParsedTopicName) Name() string {
	// Returns names like "projects/foo/topics/bar" ...
}

func (n ParsedTopicName) FullName() string {
	// Returns names like "//pubsub.googleapis.com/projects/foo/topics/bar" ...
}

You'll also get some new methods on the Topic type that the standard Go protoc generator produces:

package pubsub

func (t *Topic) ParseName() (ParsedTopicName, error) {
	return ParseTopicName(t.Name)
}

func (t *Topic) ParseFullName() (ParsedTopicName, error) {
	return ParseFullTopicName(t.Name)
}

This package also has support for google.api.resource_reference, multi-pattern resources, "externally-defined" resources, and more. However, there are some differences between this package's support for resources and Google's usage of those patterns. See "Differences from AIP / googleapis" for more on this.

Installation

It's recommended you install protoc-gen-go-resource the same way you install other build tools, like grpc-gateway, namely by describing it as a "tool" dependency, i.e. a top-level tools.go file along these lines:

// +build tools

package tools

import (
	_ "github.com/ucarion/protoc-gen-go-resource/cmd/protoc-gen-go-resource"
)

You can then install the tool by running:

go install github.com/ucarion/protoc-gen-go-resource/cmd/protoc-gen-go-resource

Supported Functionality

This section details the features supported in this package. Note that all the features below can be used in concert, but for ease of understanding they are described independently of one another.

Basic google.api.resource usage

As described at the beginning of this README, you can annotate a resource like so:

message Thing {
  option (google.api.resource) = {
    type: "example.com/Thing"
    pattern: "things/{thing}"
  };
  
  string name = 1;
}

This will generate a struct for representing a parsed Thing.name called ParsedThingName. You parse strings like things/foo into a ParsedThingName using the generated ParseThingName function, and you can generate strings like things/foo from a ParsedThingName using the Name method on ParsedThingName.

As a convenience method, you'll also get a new ParseName method added to your protoc-generated Thing type. This method is essentially just a convenience wrapper around calling ParseThingName(thing.Name).

Full resource names

As detailed in AIP-122, resources have "full resource names" in addition to their ordinary "names". The full resource name of a type is formed by combining the service name of the resource with its (non-full) name.

For instance, to take the Thing example above, the service name is example.com (the part before the slash in example.com/Thing). So this would be a Thing name:

things/foo

And this would be a Thing full name:

//example.com/things/foo

In the generated code, you can parse full names using the generated ParseFullXXXName (e.g. ParseFullThingName in this example). You can generate full names from a parsed name using the FullName method on the ParsedXXXName type (e.g. ParsedThingName).

Multi-pattern resources

Resources can have multiple patterns. For example, if a Thing can have names like:

things/foo
projects/bar/things/baz

Then you can express that by passing pattern multiple times to the google.api.resource annotation like so:

message Thing {
  option (google.api.resource) = {
    type: "example.com/Thing"
    pattern: "things/{thing}"
    pattern: "projects/{project}/things/{thing}"
  };
  
  string name = 1;
}

When generating code from a multi-pattern resource, this package will produce an interface instead of a struct for the ParsedXXXName (e.g. ParsedThingName) type. You can still call ParseXXXName / ParseFullXXXName (e.g. ParseThingName / ParseFullThingName) and the Name / FullName methods on the result, but you will need to use a type switch to handle the different possible return results.

For each possible pattern, you'll have a different concrete struct that represents that pattern. In the example above, you'll have the following structs:

type ParsedThingName_0 struct {
	ThingID string
}

type ParsedThingName_1 struct {
	ProjectID string
	ThingID   string
}

Each of those structs will implement the ParsedThingName interface, so you can write code like this to handle different possibilities:

n, err := examplepb.ParseThingName("projects/bar/things/baz")
if err != nil {
	return err
}

switch n := n.(type) {
case examplepb.ParsedThingName_0:
	fmt.Println("you gave me a project-less thing with id", n.ThingID)
case examplepb.ParsedThingName_1:
	fmt.Println("you gave me thing with id", n.ThingID, "in project", n.ProjectID)
default:
	// this code will only run if you generated code from a resource with 2+ patterns, 
	// but haven't yet updated your code to handle ParsedThingName_2 or beyond.
	//
	// ...
}

Because the generated interface looks like this:

type ParsedThingName interface {
	Name() string
	FullName() string
	
	// this will be hidden from godoc, but under the hood there's a third method
	mustEmbedParsedThingName()
}

You can always call Name or FullName on the result of parsing a multi-pattern name. However, you cannot implement the interface yourself, because there is an unexported method on the interface. This is the same code generation technique used by the Go code generator for generating oneof implementations.

Alternative name fields

By default, whenever you add a google.api.resource declaration, it's implied that the pattern values you supply refer to a protobuf field called name on the message. If you want to use a different field, you can specify a name_field on your google.api.resource call:

message Thing {
  option (google.api.resource) = {
    type: "example.com/Thing"
    pattern: "things/{thing}"
    name_field: "my_cool_name"
  };
  
  string my_cool_name = 1;
}

Specifying a non-default name_field does not affect the names of the types generated by this package. However, it does affect the name of the generated method added to Thing. Instead of generating a method called ParseName, this package will instead generate a method called ParseMyCoolName.

google.api.resource_definition

If you want to define a resource that doesn't correspond to a protobuf message, you can add the google.api.resource_definition annotation to a proto file, like so:

option (google.api.resource_definition) = {
  type: "example.com/External"
  pattern: "external/{external}"
};

This will generate the usual parser functions and types for a resource named External, namely:

type ParsedExternalName struct {
	ExternalID string
}

func ParseExternalName(s string) (ParsedExternalName, error) {
	// ...
}

func ParseFullExternalName(s string) (ParsedExternalName, error) {
	// ...
}

func (n ParsedExternalName) Name() string {
	// ...
}

func (n ParsedExternalName) FullName() string {
	// ...
}

However, since there is no Golang type associated with the resource, no convenience method for parsing a name can be generated.

You can specify google.api.resource_definition multiple times in a single .proto file. A google.api.resource_definition can have multiple patterns. The name_field of a google.api.resource_definition has no impact on the generated code.

google.api.resource_reference

You can indicate that a field contains a name of a resource using the google.api.resource_reference field annotation. When you do this, this package will generate a convenience method for parsing that field.

For example:

message Person {
  string favorite_thing = 1 [(google.api.resource_reference) = {
    type: "example.com/Thing"
  }];
}

message Thing {
  option (google.api.resource) = {
    type: "example.com/Thing"
    pattern: "things/{thing}"
  };

  string name = 1;
}

Will generate, in addition to the usual set of generated code for Thing, a new method on the Person:

func (p *Person) ParseFavoriteThing() (ParsedThingName, error) {
	return ParseThingName(p.FavoriteThing)
}

This package determines which type and parsing function to use thanks to the type you provide to google.api.resource_reference, and matching it against a google.api.resource or google.api.resource_definition declaration.

Differences from AIP / googleapis

The google.api.resource, google.api.resource_definition, and google.api.resource_reference annotations are loosely specified in three places:

  • Google's API Improvement Proposals, especially AIP-122, AIP-123, AIP-180, and AIP-4231, describe how the annotations are meant to be used.
  • Google's api-common-protos contains declarations of these annotations that can be used by protoc and imported by .proto files.
  • Google's googleapis contains the interface definitions of many of Google's public APIs. These provide real-world examples of the annotations in use.

Based on the information from these sources, there are known differences between how this package interprets the annotations versus how Google appears to interpret them internally.

No support for tilde separators

Google supports use of tilde as separators within a path segment. For instance, consider AdGroupAd in googleapis:

message AdGroupAd {
  option (google.api.resource) = {
    type: "googleads.googleapis.com/AdGroupAd"
    pattern: "customers/{customer_id}/adGroupAds/{ad_group_id}~{ad_id}"
  };

  // ...
}

This package does not support such patterns, but such support may be added at a later date. This functionality is not yet implemented simply because of the complexity required to generate code for parsing such patterns, not for any deep technical reason.

Name field must exist

Google's use of the resource annotation does not appear to require that that name field, name by default, does not need to exist. This package considers the name field not existing to be an error.

AdGroupAd in the previous section is an example of a message that specifies name as the name field, but doesn't have a name field.

No support for child_type

AIP-122 documents that, for resources which may have multiple different "parent" types, google.api.resource_reference may use child_type instead of type to indicate that the referred-to type is any one of the types which may be a parent of the given child_type.

This package does not support child_type, because such support would require that this package be aware of the parent-child relationships of all resources. This information is not explicitly provided in the annotations this package understands. Instead, parent-child relationships are implicit in the shape of the patterns of resource names.

Determining parent-child relationships from annotations would require doing some inference based on the patterns of names for each type. Although this is doable, for the result to be usable in practice, some additional tooling would be required to make it clear what parent/child relationships the code generator is inferring, so that developers can debug what's going wrong if the generated code isn't as expected.

As a result, child_type support may be added in the future, but will not be included without additional tooling for users to be able to gain insight into the code generator's inferences.

# Packages

No description provided by the author