blob: 2ab9b8f8050bfcf6b01d258b266bc0d701780899 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This file implements output formatting for the cobalt config parser.
package source_generator
import (
"bytes"
"config"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
)
type outputLanguage interface {
getCommentPrefix() string
supportsTypeAlias() bool
writeExtraHeader(so *sourceOutputter, projectName, customerName string, namespaces []string)
writeExtraFooter(so *sourceOutputter, projectName, customerName string, namespaces []string)
writeEnumBegin(so *sourceOutputter, name ...string)
writeEnumEntry(so *sourceOutputter, value uint32, name ...string)
writeEnumAliasesBegin(so *sourceOutputter, name ...string)
writeEnumAlias(so *sourceOutputter, name, from, to []string)
writeEnumEnd(so *sourceOutputter, name ...string)
writeEnumExport(so *sourceOutputter, enumName, name []string)
writeTypeAlias(so *sourceOutputter, from, to []string)
writeNamespacesBegin(so *sourceOutputter, namespaces []string, outputFilename string)
writeNamespacesEnd(so *sourceOutputter, namespaces []string, outputFilename string)
writeConstUint32(so *sourceOutputter, value uint32, name ...string)
writeConstInt64(so *sourceOutputter, value int64, name ...string)
writeStringConstant(so *sourceOutputter, value string, name ...string)
writeStructBegin(so *sourceOutputter, name ...string)
writeStructField(so *sourceOutputter, name, typeName []string)
writeToVectorMethod(so *sourceOutputter, structName []string, fields [][]string)
writeStructEnd(so *sourceOutputter)
}
type OutputFormatter func(c, filtered *config.CobaltRegistry) (outputBytes []byte, err error)
type generatorFeatures struct {
enabledFeatures map[string]bool
}
func newGeneratorFeatures(features []string) generatorFeatures {
enabledFeatures := map[string]bool{}
for _, feature := range features {
enabledFeatures[feature] = true
}
return generatorFeatures{enabledFeatures: enabledFeatures}
}
func (f *generatorFeatures) isForTesting() bool {
return f.enabledFeatures["testing"]
}
func (f *generatorFeatures) shouldGenerateConfigBase64() bool {
return f.enabledFeatures["generate-config-base64"]
}
type sourceOutputter struct {
buffer *bytes.Buffer
language outputLanguage
varName string
namespaces []string
features generatorFeatures
outFilename string
}
func newSourceOutputter(language outputLanguage, varName string, features []string) *sourceOutputter {
return newSourceOutputterWithNamespaces(language, varName, []string{}, features)
}
func newSourceOutputterWithNamespaces(language outputLanguage, varName string, namespaces []string, features []string) *sourceOutputter {
return newSourceOutputterWithNamespacesAndFilename(language, varName, namespaces, features, "")
}
func newSourceOutputterWithNamespacesAndFilename(language outputLanguage, varName string, namespaces []string, features []string, outFilename string) *sourceOutputter {
return &sourceOutputter{
buffer: new(bytes.Buffer),
language: language,
varName: varName,
namespaces: namespaces,
features: newGeneratorFeatures(features),
outFilename: outFilename,
}
}
func (so *sourceOutputter) writeLine(str string) {
so.buffer.WriteString(str + "\n")
}
func (so *sourceOutputter) writeLineFmt(format string, args ...interface{}) {
so.writeLine(fmt.Sprintf(format, args...))
}
func (so *sourceOutputter) writeComment(comment string) {
for _, comment_line := range strings.Split(comment, "\n") {
so.writeLineFmt("%s %s", so.language.getCommentPrefix(), strings.TrimLeft(comment_line, " "))
}
}
func (so *sourceOutputter) writeCommentFmt(format string, args ...interface{}) {
so.writeComment(fmt.Sprintf(format, args...))
}
func (so *sourceOutputter) writeGenerationWarning() {
so.writeCommentFmt(`This file was generated by Cobalt's Registry parser based on the registry YAML
in the cobalt_config repository. Edit the YAML there to make changes.`)
}
type idEntry struct {
id uint32
index int
}
// writeIdConstants prints out a list of constants to be used in testing. It
// uses the Name attribute of each Metric and Report to construct the constants.
//
// For a metric named "SingleString" the cpp constant would be kSingleStringMetricId
// For a report named "Test" in the "SingleString" metric, the cpp constant would be kSingleStringTestReportId
func (so *sourceOutputter) writeIdConstants(constType string, entries map[string]idEntry) {
if len(entries) == 0 {
return
}
var keys []string
for k := range entries {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if entries[keys[i]].id != entries[keys[j]].id {
return entries[keys[i]].id < entries[keys[j]].id
}
return keys[i] < keys[j]
})
so.writeCommentFmt("%s ID Constants", constType)
for _, name := range keys {
id := entries[name].id
so.writeComment(name)
so.language.writeConstUint32(so, id, name, constType, "id")
}
if so.features.isForTesting() {
so.writeLine("")
so.writeCommentFmt("Index into the list of %ss. Used to look up %ss in the registry proto directly.", constType, constType)
for _, name := range keys {
index := entries[name].index
so.language.writeConstUint32(so, uint32(index), name, constType, "index")
}
}
so.writeLine("")
}
type EnumName struct {
prefix, suffix string
}
type EnumEntry struct {
events map[uint32]string
aliases map[string]string
names []EnumName
}
// writeEnum prints out an enum with a for list of EventCodes (cobalt v1.0 only)
//
// It prints out the event_code string using toIdent, (event_code => EventCode).
// In c++ and Dart, it also creates a series of constants that effectively
// export the enum values. For a metric called "foo_bar" with a event named
// "baz", it would generate the constant:
// "FooBarEventCode_Baz = FooBarEventCode::Baz"
func (so *sourceOutputter) writeEnum(entry EnumEntry, projectName string, allEnums []EnumEntry) {
if len(entry.events) == 0 {
return
}
if len(entry.names) == 0 {
return
}
var keys []uint32
for k := range entry.events {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
hasGlobalName := so.language.supportsTypeAlias()
firstSuffix := entry.names[0].suffix
if so.language.supportsTypeAlias() {
// Verify that all suffixes are the same for this entry
for _, name := range entry.names {
if name.suffix != firstSuffix {
hasGlobalName = false
}
}
// Verify that no other entry has this suffix
for _, enum := range allEnums {
if !reflect.DeepEqual(enum, entry) {
for _, name := range enum.names {
if name.suffix == firstSuffix {
hasGlobalName = false
}
}
}
}
}
projectNamePrefix := projectName
for i, name := range entry.names {
if i == 0 || !so.language.supportsTypeAlias() {
prefix := name.prefix
if hasGlobalName {
prefix = projectNamePrefix
}
so.writeCommentFmt("Enum for %s (%s)", prefix, name.suffix)
so.language.writeEnumBegin(so, prefix, name.suffix)
for _, id := range keys {
name := entry.events[id]
so.language.writeEnumEntry(so, id, name)
}
if len(entry.aliases) > 0 {
so.language.writeEnumAliasesBegin(so, prefix, name.suffix)
for from, to := range entry.aliases {
so.language.writeEnumAlias(so, []string{prefix, name.suffix}, []string{from}, []string{to})
}
}
so.language.writeEnumEnd(so, prefix, name.suffix)
if hasGlobalName {
so.writeCommentFmt("Alias for %s (%s) which has the same event codes", name.prefix, name.suffix)
so.language.writeTypeAlias(so, []string{prefix, name.suffix}, []string{name.prefix, name.suffix})
}
} else {
first := entry.names[0]
if hasGlobalName {
first = EnumName{prefix: projectNamePrefix, suffix: first.suffix}
}
so.writeCommentFmt("Alias for %s (%s) which has the same event codes", name.prefix, name.suffix)
so.language.writeTypeAlias(so, []string{first.prefix, first.suffix}, []string{name.prefix, name.suffix})
}
for _, id := range keys {
so.language.writeEnumExport(so, []string{name.prefix, name.suffix}, []string{entry.events[id]})
}
if len(entry.aliases) > 0 {
for _, to := range entry.aliases {
so.language.writeEnumExport(so, []string{name.prefix, name.suffix}, []string{to})
}
}
so.writeLine("")
}
}
func (so *sourceOutputter) Bytes() []byte {
return so.buffer.Bytes()
}
func (so *sourceOutputter) writeIntBuckets(buckets *config.IntegerBuckets, name ...string) {
linear := buckets.GetLinear()
if linear != nil {
so.writeCommentFmt("Linear bucket constants for %s", strings.Join(name, " "))
name = append(name, "int buckets")
so.language.writeConstInt64(so, linear.Floor, append(name, "floor")...)
so.language.writeConstUint32(so, linear.NumBuckets, append(name, "num buckets")...)
so.language.writeConstUint32(so, linear.StepSize, append(name, "step size")...)
so.writeLine("")
}
exponential := buckets.GetExponential()
if exponential != nil {
so.writeCommentFmt("Exponential bucket constants for %s", strings.Join(name, " "))
name = append(name, "int buckets")
so.language.writeConstInt64(so, exponential.Floor, append(name, "floor")...)
so.language.writeConstUint32(so, exponential.NumBuckets, append(name, "num buckets")...)
so.language.writeConstUint32(so, exponential.InitialStep, append(name, "initial step")...)
so.language.writeConstUint32(so, exponential.StepMultiplier, append(name, "step multiplier")...)
so.writeLine("")
}
}
func (so *sourceOutputter) writeV1Constants(c *config.CobaltRegistry) error {
metrics := make(map[string]idEntry)
var reports []map[string]idEntry
if len(c.Customers) > 1 || len(c.Customers[0].Projects) > 1 {
return fmt.Errorf("Cobalt v1.0 output can only be used with a single project registry.")
}
so.writeNames(c)
so.writeLine("")
for i, metric := range c.Customers[0].Projects[0].Metrics {
if metric.MetricName != "" {
so.writeIntBuckets(metric.GetIntBuckets(), metric.MetricName)
metrics[metric.MetricName] = idEntry{id: metric.Id, index: i}
metric_reports := make(map[string]idEntry)
for j, report := range metric.Reports {
if report.ReportName != "" {
so.writeIntBuckets(report.GetIntBuckets(), metric.MetricName, report.ReportName)
metric_reports[fmt.Sprintf("%s %s", metric.MetricName, report.ReportName)] = idEntry{id: report.Id, index: j}
}
}
reports = append(reports, metric_reports)
}
}
so.writeIdConstants("Metric", metrics)
if so.features.isForTesting() {
for _, metric_reports := range reports {
so.writeIdConstants("Report", metric_reports)
}
}
var enums []EnumEntry
for _, metric := range c.Customers[0].Projects[0].Metrics {
if len(metric.MetricDimensions) > 0 {
for i, md := range metric.MetricDimensions {
events := make(map[uint32]string)
for value, name := range md.EventCodes {
events[value] = name
}
varname := "Metric Dimension " + strconv.Itoa(i)
if md.Dimension != "" {
varname = "Metric Dimension " + md.Dimension
}
found := false
for i, e := range enums {
if reflect.DeepEqual(e.events, events) && reflect.DeepEqual(e.aliases, md.EventCodeAliases) {
enums[i].names = append(enums[i].names, EnumName{prefix: metric.MetricName, suffix: varname})
found = true
break
}
}
if !found {
enums = append(enums, EnumEntry{
events: events,
aliases: md.EventCodeAliases,
names: []EnumName{
{
prefix: metric.MetricName,
suffix: varname,
},
},
})
}
}
}
}
for _, e := range enums {
so.writeEnum(e, c.Customers[0].Projects[0].ProjectName, enums)
}
for _, metric := range c.Customers[0].Projects[0].Metrics {
definedDimensions := 0
for _, md := range metric.MetricDimensions {
if len(md.EventCodes) > 0 {
definedDimensions += 1
}
}
// If there are 2 or more MetricDimensions, we will generate a helper struct for managing them.
if definedDimensions > 1 && definedDimensions == len(metric.MetricDimensions) {
so.language.writeStructBegin(so, metric.MetricName, "event codes")
fields := [][]string{}
for i, md := range metric.MetricDimensions {
if len(md.EventCodes) > 0 {
name := "Dimension " + strconv.Itoa(i)
varname := "Metric Dimension " + strconv.Itoa(i)
if md.Dimension != "" {
name = md.Dimension
varname = "Metric Dimension " + md.Dimension
}
fields = append(fields, []string{name})
so.language.writeStructField(so, []string{name}, []string{metric.MetricName, varname})
}
}
so.language.writeToVectorMethod(so, []string{metric.MetricName, "event codes"}, fields)
so.language.writeStructEnd(so)
so.writeLine("")
}
}
return nil
}
func (so *sourceOutputter) writeNames(c *config.CobaltRegistry) {
so.language.writeStringConstant(so, c.Customers[0].CustomerName, "Customer Name")
so.language.writeConstUint32(so, c.Customers[0].CustomerId, "Customer Id")
so.language.writeStringConstant(so, c.Customers[0].Projects[0].ProjectName, "Project Name")
so.language.writeConstUint32(so, c.Customers[0].Projects[0].ProjectId, "Project Id")
}
func (so *sourceOutputter) writeFile(c, filtered *config.CobaltRegistry) error {
so.writeGenerationWarning()
customer := ""
project := ""
for _, cust := range c.Customers {
customer = strings.TrimLeft(customer+"_"+cust.CustomerName, "_")
for _, proj := range cust.Projects {
project = strings.TrimLeft(project+"_"+proj.ProjectName, "_")
}
}
so.language.writeExtraHeader(so, customer, project, so.namespaces)
so.language.writeNamespacesBegin(so, so.namespaces, so.outFilename)
if len(c.Customers) == 1 && len(c.Customers[0].Projects) == 1 {
if err := so.writeV1Constants(c); err != nil {
return err
}
}
b64Bytes, err := Base64Output(c, filtered)
if err != nil {
return err
}
if so.features.shouldGenerateConfigBase64() {
so.writeComment("The base64 encoding of the bytes of a serialized CobaltRegistry proto message.")
so.language.writeStringConstant(so, string(b64Bytes), so.varName)
}
so.language.writeNamespacesEnd(so, so.namespaces, so.outFilename)
so.language.writeExtraFooter(so, customer, project, so.namespaces)
return nil
}
func (so *sourceOutputter) getOutputFormatter() OutputFormatter {
return func(c, filtered *config.CobaltRegistry) (outputBytes []byte, err error) {
err = so.writeFile(c, filtered)
return so.Bytes(), err
}
}
func getOutputFormatter(format, namespace, goPackageName, varName string, features []string, outFilename string) (OutputFormatter, error) {
namespaceList := []string{}
if namespace != "" {
namespaceList = strings.Split(namespace, ".")
}
switch format {
case "bin":
return BinaryOutput, nil
case "b64":
return Base64Output, nil
case "cpp":
return CppOutputFactory(varName, namespaceList, features), nil
case "dart":
return DartOutputFactory(varName, features), nil
case "go":
if goPackageName == "" {
return nil, fmt.Errorf("no package name specified for go output formatter")
}
return GoOutputFactory(varName, goPackageName, features), nil
case "java":
return JavaOutputFactory(varName, namespaceList, features, outFilename), nil
case "json":
return JSONOutput, nil
case "rust":
return RustOutputFactory(varName, namespaceList, features), nil
default:
return nil, fmt.Errorf("'%v' is an invalid out_format parameter. 'bin', 'b64', 'cpp', 'dart', 'go', 'java', 'rust', and 'json' are the only valid values for out_format.", format)
}
}