Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create genyaml library to generate commented YAML from Go structs. #13637

Merged
merged 9 commits into from Aug 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions BUILD.bazel
Expand Up @@ -119,6 +119,7 @@ filegroup(
"//metrics:all-srcs",
"//pkg/benchmarkjunit:all-srcs",
"//pkg/flagutil:all-srcs",
"//pkg/genyaml:all-srcs",
"//pkg/ghclient:all-srcs",
"//pkg/io:all-srcs",
"//prow:all-srcs",
Expand Down
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -99,6 +99,7 @@ require (
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107
google.golang.org/grpc v1.19.1
gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5
gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22
k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b
k8s.io/apiextensions-apiserver v0.0.0-20190409022649-727a075fdec8
k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -498,6 +498,8 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22 h1:0efs3hwEZhFKsCoP8l6dDB1AZWMgnEl3yWXWRZTOaEA=
gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
80 changes: 80 additions & 0 deletions pkg/genyaml/BUILD.bazel
@@ -0,0 +1,80 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = ["genyaml.go"],
data = ["//prow/plugins:config-src"],
importpath = "k8s.io/test-infra/pkg/genyaml",
visibility = ["//visibility:public"],
deps = ["//vendor/gopkg.in/yaml.v3:go_default_library"],
)

go_test(
name = "go_default_test",
srcs = ["genyaml_test.go"],
data = [":test-data"],
embed = [":go_default_library"],
deps = [
"//pkg/genyaml/testdata/alias_types:go_default_library",
"//pkg/genyaml/testdata/embedded_structs:go_default_library",
"//pkg/genyaml/testdata/interface_types:go_default_library",
"//pkg/genyaml/testdata/multiline_comments:go_default_library",
"//pkg/genyaml/testdata/nested_structs:go_default_library",
"//pkg/genyaml/testdata/no_tags:go_default_library",
"//pkg/genyaml/testdata/omit_if_empty:go_default_library",
"//pkg/genyaml/testdata/pointer_types:go_default_library",
"//pkg/genyaml/testdata/primitive_types:go_default_library",
"//pkg/genyaml/testdata/private_members:go_default_library",
"//vendor/gopkg.in/yaml.v3:go_default_library",
],
)

filegroup(
name = "test-data",
srcs = [
"//pkg/genyaml/testdata/alias_types:test-data",
"//pkg/genyaml/testdata/embedded_structs:test-data",
"//pkg/genyaml/testdata/inject_comments:test-data",
"//pkg/genyaml/testdata/interface_types:test-data",
"//pkg/genyaml/testdata/multiline_comments:test-data",
"//pkg/genyaml/testdata/multiple_paths:test-data",
"//pkg/genyaml/testdata/nested_structs:test-data",
"//pkg/genyaml/testdata/no_tags:test-data",
"//pkg/genyaml/testdata/omit_if_empty:test-data",
"//pkg/genyaml/testdata/pointer_types:test-data",
"//pkg/genyaml/testdata/primitive_types:test-data",
"//pkg/genyaml/testdata/private_members:test-data",
"//pkg/genyaml/testdata/set_path_overwrite:test-data",
"//pkg/genyaml/testdata/single_path:test-data",
],
)

filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)

filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//pkg/genyaml/testdata/alias_types:all-srcs",
"//pkg/genyaml/testdata/embedded_structs:all-srcs",
"//pkg/genyaml/testdata/inject_comments:all-srcs",
"//pkg/genyaml/testdata/interface_types:all-srcs",
"//pkg/genyaml/testdata/multiline_comments:all-srcs",
"//pkg/genyaml/testdata/multiple_paths:all-srcs",
"//pkg/genyaml/testdata/nested_structs:all-srcs",
"//pkg/genyaml/testdata/no_tags:all-srcs",
"//pkg/genyaml/testdata/omit_if_empty:all-srcs",
"//pkg/genyaml/testdata/pointer_types:all-srcs",
"//pkg/genyaml/testdata/primitive_types:all-srcs",
"//pkg/genyaml/testdata/private_members:all-srcs",
"//pkg/genyaml/testdata/set_path_overwrite:all-srcs",
"//pkg/genyaml/testdata/single_path:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
190 changes: 190 additions & 0 deletions pkg/genyaml/README.md
@@ -0,0 +1,190 @@
# Genyaml

## Description
`genyaml` is a simple documentation tool used to marshal YAML from Golang structs. It extracts *doc comments* from `.go` sources files and adds them as *comment nodes* in the YAML output.

## Usage

TODOs are ignored (e.g. TODO(clarketm)... or TODO...) if and only if they are on a **single** line.

```go
type Employee struct {
// Name of employee
Name string
// Age of employee
Age int
// TODO(clarketm): change this to a float64
// Salary of employee
Salary int
}
```

```yaml
# Age of employee
Age: 22

# Name of employee
Name: Jim

# Salary of employee
Salary: 100000
```

Multiline comments are preserved, albeit *tabs* are converted to *spaces* and *multiple* spaces are compressed into a *single* line.

```go
type Multiline struct {
// StringField1 comment
// second line
// third line
StringField1 string `json:"string1"`

/* StringField2 comment
second line
third line
*/
StringField2 string `json:"string2"`

/* StringField3 comment
second line
third line
*/
StringField3 string `json:"string3"`
}
```

```yaml
# StringField1 comment
# second line
# third line
string1: string1

# StringField2 comment
# second line
# third line
string2: string2

# StringField3 comment
# second line
# third line
string3: string3
```

All subsequent lines and blocks after a `---` will be ignored.

```go
type Person struct {
// Name of person
// ---
// The name of the person is both the first and last name separated
// by a space character
Name string
}
```

```yaml
# Name of person
Name: Frank
```

Generator instructions prefixed with a `+` are ignored.

```go
type Dog struct {
// Gender of dog (male|female)
// +optional
Gender string `json:"gender,omitempty"`
// Weight in pounds of dog
Weight int `json:"weight,omitempty"`
}
```

```yaml
# Gender of dog (male|female)
gender: male

# Weight in pounds of dog
weight: 150
```

## Example

First, assume there is a Go file `config.go` with the following contents:
> NOTE: `genyaml` reads **json** tags for maximum portability.

```go
// config.go

package example

type Configuration struct {
// Plugin comment
Plugin []Plugin `json:"plugin,omitempty"`
}

type Plugin struct {
// StringField comment
StringField string `json:"string,omitempty"`
// BooleanField comment
BooleanField bool `json:"boolean,omitempty"`
// IntegerField comment
IntegerField int `json:"integer,omitempty"`
}
//...
```

Next, in a separate `example.go` file, initialize a `Configuration` struct and marshal it to *commented* YAML.

```go
// example.go

package example

// Import genyaml
import "k8s.io/test-infra/pkg/genyaml"

//...

// Initialize a `Configuration` struct:
config := &example.Configuration{
Plugin: []example.Plugin{
{
StringField: "string",
BooleanField: true,
IntegerField: 1,
},
},
}

// Initialize a CommentMap instance from the `config.go` source file:
cm := genyaml.NewCommentMap("config.go")

// Generate a commented YAML snippet:
yamlSnippet, err := cm.GenYaml(config)
```

The doc comments are extracted from the `config.go` file and attached to the corresponding YAML fields:

```go
fmt.Println(yamlSnippet)
```

```yaml
# Plugin comment
plugin:
- # BooleanField comment
boolean: true

# IntegerField comment
integer: 1

# StringField comment
string: string

```

## Limitations / Going Forward

- [ ] Embedded structs must include a tag name (i.e. must not be *spread*), otherwise the type can not be inferred from the YAML output.
- [ ] Interface types, more specifically concrete types implementing a particular interface, can can not be inferred from the YAML output.
- [ ] Upstream this functionality to `go-yaml` (or a fork) to leverage custom formatting of YAML and direct reflection on types for resolving embedded structs and interface types across multiple source files.