blob: 4a626931d14c6381a26855c41f3a8a22fff1372b [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)
writeNamespacesEnd(so *sourceOutputter, namespaces []string)
writeConstUint32(so *sourceOutputter, value uint32, name ...string)
writeConstInt64(so *sourceOutputter, value int64, name ...string)
writeStringConstant(so *sourceOutputter, value string, name ...string)
}
type OutputFormatter func(c, filtered *config.CobaltRegistry) (outputBytes []byte, err error)
type sourceOutputter struct {
buffer *bytes.Buffer
language outputLanguage
varName string
namespaces []string
forTesting bool
}
func newSourceOutputter(language outputLanguage, varName string, forTesting bool) *sourceOutputter {
return newSourceOutputterWithNamespaces(language, varName, []string{}, forTesting)
}
func newSourceOutputterWithNamespaces(language outputLanguage, varName string, namespaces []string, forTesting bool) *sourceOutputter {
return &sourceOutputter{
buffer: new(bytes.Buffer),
language: language,
varName: varName,
namespaces: namespaces,
forTesting: forTesting,
}
}
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 Config parser based on the configuration
YAML in the cobalt_config repository. Edit the YAML there to make changes.`)
}
// 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]uint32) {
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]] != entries[keys[j]] {
return entries[keys[i]] < entries[keys[j]]
}
return keys[i] < keys[j]
})
so.writeCommentFmt("%s ID Constants", constType)
for _, name := range keys {
id := entries[name]
so.writeComment(name)
so.language.writeConstUint32(so, id, name, constType, "id")
}
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) {
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] })
for i, name := range entry.names {
if i == 0 || !so.language.supportsTypeAlias() {
so.writeCommentFmt("Enum for %s (%s)", name.prefix, name.suffix)
so.language.writeEnumBegin(so, name.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, name.prefix, name.suffix)
for from, to := range entry.aliases {
so.language.writeEnumAlias(so, []string{name.prefix, name.suffix}, []string{from}, []string{to})
}
}
so.language.writeEnumEnd(so, name.prefix, name.suffix)
} else {
first := entry.names[0]
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]uint32)
reports := make(map[string]uint32)
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 config.")
}
so.writeNames(c)
so.writeLine("")
for _, metric := range c.Customers[0].Projects[0].Metrics {
if metric.MetricName != "" {
so.writeIntBuckets(metric.GetIntBuckets(), metric.MetricName)
metrics[metric.MetricName] = metric.Id
for _, report := range metric.Reports {
if report.ReportName != "" {
so.writeIntBuckets(report.GetIntBuckets(), metric.MetricName, report.ReportName)
reports[fmt.Sprintf("%s %s", metric.MetricName, report.ReportName)] = report.Id
}
}
}
}
so.writeIdConstants("Metric", metrics)
if so.forTesting {
so.writeIdConstants("Report", 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)
}
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)
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
}
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.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, forTesting bool) (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, forTesting), nil
case "dart":
return DartOutputFactory(varName, forTesting), nil
case "go":
if goPackageName == "" {
return nil, fmt.Errorf("no package name specified for go output formatter")
}
return GoOutputFactory(varName, goPackageName, forTesting), nil
case "json":
return JSONOutput, nil
case "rust":
return RustOutputFactory(varName, namespaceList, forTesting), nil
default:
return nil, fmt.Errorf("'%v' is an invalid out_format parameter. 'bin', 'b64', 'cpp', 'dart', 'go', 'rust', and 'json' are the only valid values for out_format.", format)
}
}