| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "context" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "sort" |
| "strings" |
| "text/template" |
| "unicode" |
| |
| "github.com/golang/protobuf/proto" |
| |
| "go.chromium.org/luci/common/errors" |
| luciproto "go.chromium.org/luci/common/proto" |
| |
| "infra/libs/bqschema/tabledef" |
| ) |
| |
| func sanitizeComment(v string) string { |
| prevSpace := false |
| v = strings.Map(func(r rune) rune { |
| switch { |
| case unicode.IsSpace(r): |
| if prevSpace { |
| return -1 |
| } |
| prevSpace = true |
| return ' ' |
| case unicode.IsPrint(r): |
| prevSpace = false |
| return r |
| default: |
| return -1 |
| } |
| }, v) |
| return strings.TrimSpace(v) |
| } |
| |
| var structTemplate = template.Must(template.New(""). |
| Funcs(template.FuncMap{ |
| "sanitizeComment": sanitizeComment, |
| }). |
| Parse(` |
| // THIS FILE IS AUTOGENERATED. DO NOT MODIFY. |
| |
| package {{.Package}} |
| |
| import pb "infra/libs/bqschema/tabledef" |
| {{range .Imports -}} |
| import {{.Alias}} "{{.Path}}" |
| {{end -}} |
| |
| // {{.TableDefName}} is the TableDef for the |
| // "{{.DatasetID}}" dataset's "{{.TableID}}" table. |
| var {{.TableDefName}} = &pb.TableDef{ |
| DatasetId: "{{.DatasetID}}", |
| TableId: "{{.TableID}}", |
| } |
| {{range .Structs -}} |
| |
| // {{sanitizeComment .Comment}} |
| type {{.Name}} struct { |
| {{range .Fields -}} |
| {{if .Description -}} |
| // {{sanitizeComment .Description}} |
| {{end -}} |
| {{.Name}} {{.Type}} ` + "`bigquery:\"{{.FieldName}}\"`" + ` |
| |
| {{end -}} |
| } |
| {{end -}} |
| `)) |
| |
| // LoadTableDef loads a TableDef text protobuf. |
| func LoadTableDef(path string) (*tabledef.TableDef, error) { |
| content, err := ioutil.ReadFile(path) |
| if err != nil { |
| return nil, err |
| } |
| |
| var tdef tabledef.TableDef |
| return &tdef, luciproto.UnmarshalTextML(string(content), &tdef) |
| } |
| |
| // Export exports the specified TableDef as a set of Go structs for the |
| // specific package. The primary exported struct will be named "structName", |
| // and the associated TableDef will be named "<structName>Table". |
| // |
| // The resulting generated Go source will be written to "out". |
| func Export(ctx context.Context, td *tabledef.TableDef, packageName, structName, out string) error { |
| tableDefName := structName + "Table" |
| |
| type packageImport struct { |
| Alias string |
| Path string |
| } |
| |
| type params struct { |
| Package string |
| Imports []*packageImport |
| TableDefName string |
| DatasetID string |
| TableID string |
| Structs []*structDef |
| } |
| |
| a := analyzer{ |
| structBase: structName, |
| } |
| a.ensureStruct("", fmt.Sprintf("the schema for %q.", tableDefName), td.Fields) |
| |
| p := params{ |
| Package: packageName, |
| Imports: make([]*packageImport, 0, len(a.packages)), |
| TableDefName: tableDefName, |
| DatasetID: td.DatasetId, |
| TableID: td.TableId, |
| Structs: a.structDefs, |
| } |
| |
| // Add sorted package imports. |
| for alias, path := range a.packages { |
| p.Imports = append(p.Imports, &packageImport{alias, path}) |
| } |
| sort.Slice(p.Imports, func(i, j int) bool { return p.Imports[i].Alias < p.Imports[j].Alias }) |
| |
| // Generate the template. |
| fd, err := os.Create(out) |
| if err != nil { |
| return errors.Annotate(err, "could not open output file %q", out).Err() |
| } |
| err = structTemplate.Execute(fd, &p) |
| if err != nil { |
| _ = fd.Close() |
| return errors.Annotate(err, "could not generate output file").Err() |
| } |
| if err := fd.Close(); err != nil { |
| return errors.Annotate(err, "could not close output file").Err() |
| } |
| |
| cmd := exec.CommandContext(ctx, "gofmt", "-s", "-w", out) |
| if err := cmd.Run(); err != nil { |
| return errors.Annotate(err, "could not format output file").Err() |
| } |
| |
| return nil |
| } |
| |
| type structDef struct { |
| Comment string |
| Name string |
| Fields []*fieldEntry |
| } |
| |
| type fieldEntry struct { |
| Name string |
| Type string |
| FieldName string |
| Description string |
| } |
| |
| type analyzer struct { |
| structBase string |
| packages map[string]string |
| |
| structs map[string]*structDef |
| structDefs []*structDef |
| } |
| |
| func (a *analyzer) ensurePackage(name, path string) { |
| if _, ok := a.packages[name]; ok { |
| return |
| } |
| if a.packages == nil { |
| a.packages = make(map[string]string) |
| } |
| a.packages[name] = path |
| } |
| |
| func (a *analyzer) ensureStruct(fieldName, comment string, schema []*tabledef.FieldSchema) string { |
| // Create "key", a deterministic rendering of "schema". |
| keyBaser := tabledef.FieldSchema{ |
| Schema: schema, |
| } |
| schemaBytes, err := proto.Marshal(&keyBaser) |
| if err != nil { |
| panic(err) |
| } |
| key := string(schemaBytes) |
| |
| // Is an identical struct already defined? |
| if sd := a.structs[key]; sd != nil { |
| return sd.Name |
| } |
| |
| name := a.structBase |
| if fieldName != "" { |
| name = fmt.Sprintf("%s_%s", name, toCamelCase(fieldName)) |
| if comment == "" { |
| comment = fmt.Sprintf("a record for the %q field.", fieldName) |
| } |
| } |
| |
| if comment != "" { |
| comment = fmt.Sprintf("%s is %s", name, comment) |
| } |
| |
| // Define a new struct type. |
| sd := structDef{ |
| Name: name, |
| Fields: make([]*fieldEntry, len(schema)), |
| Comment: comment, |
| } |
| for i, field := range schema { |
| sd.Fields[i] = a.makeFieldEntry(field) |
| } |
| if a.structs == nil { |
| a.structs = make(map[string]*structDef) |
| } |
| a.structs[key] = &sd |
| a.structDefs = append(a.structDefs, &sd) |
| return sd.Name |
| } |
| |
| func (a *analyzer) makeFieldEntry(fs *tabledef.FieldSchema) *fieldEntry { |
| var typ string |
| switch fs.Type { |
| case tabledef.Type_STRING: |
| typ = "string" |
| case tabledef.Type_BYTES: |
| typ = "[]byte" |
| case tabledef.Type_INTEGER: |
| typ = "int64" |
| case tabledef.Type_FLOAT: |
| typ = "float64" |
| case tabledef.Type_BOOLEAN: |
| typ = "bool" |
| case tabledef.Type_TIMESTAMP: |
| a.ensurePackage("time", "time") |
| typ = "time.Time" |
| case tabledef.Type_RECORD: |
| typ = "*" + a.ensureStruct(fs.Name, "", fs.Schema) |
| case tabledef.Type_DATE: |
| a.ensurePackage("civil", "cloud.google.com/go/civil") |
| typ = "civil.Date" |
| case tabledef.Type_TIME: |
| a.ensurePackage("civil", "cloud.google.com/go/civil") |
| typ = "civil.Time" |
| case tabledef.Type_DATETIME: |
| a.ensurePackage("civil", "cloud.google.com/go/civil") |
| typ = "civil.DateTime" |
| } |
| if fs.IsRepeated { |
| typ = "[]" + typ |
| } |
| |
| return &fieldEntry{ |
| Name: toCamelCase(fs.Name), |
| Type: typ, |
| FieldName: fs.Name, |
| Description: fs.Description, |
| } |
| } |