| // 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" |
| "privacy" |
| "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) |
| writeConstMap(so *sourceOutputter, value map[uint32]string, 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 sourceOutputter struct { |
| buffer *bytes.Buffer |
| language outputLanguage |
| varName string |
| namespaces []string |
| options generatorOptions |
| outFilename string |
| } |
| |
| func newSourceOutputter(language outputLanguage, varName string, options generatorOptions) *sourceOutputter { |
| return newSourceOutputterWithNamespaces(language, varName, []string{}, options) |
| } |
| |
| func newSourceOutputterWithNamespaces(language outputLanguage, varName string, namespaces []string, options generatorOptions) *sourceOutputter { |
| return newSourceOutputterWithNamespacesAndFilename(language, varName, namespaces, options, "") |
| } |
| |
| func newSourceOutputterWithNamespacesAndFilename(language outputLanguage, varName string, namespaces []string, options generatorOptions, outFilename string) *sourceOutputter { |
| return &sourceOutputter{ |
| buffer: new(bytes.Buffer), |
| language: language, |
| varName: varName, |
| namespaces: namespaces, |
| options: options, |
| 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.options.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 |
| addNameMap bool |
| } |
| |
| 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 _, name := range entry.names { |
| if name.prefix == projectNamePrefix { |
| projectNamePrefix = projectNamePrefix + " project" |
| } |
| } |
| |
| for i, name := range entry.names { |
| if i == 0 || !so.language.supportsTypeAlias() { |
| prefix := name.prefix |
| if hasGlobalName { |
| prefix = projectNamePrefix |
| } |
| |
| if name.addNameMap { |
| so.writeCommentFmt("String map for %s (%s)", prefix, name.suffix) |
| so.language.writeConstMap(so, entry.events, name.prefix, name.suffix, "name map") |
| so.writeLine("") |
| } |
| |
| 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}) |
| |
| if name.addNameMap { |
| so.writeCommentFmt("String map for %s (%s)", name.prefix, name.suffix) |
| so.language.writeConstMap(so, entry.events, name.prefix, name.suffix, "name map") |
| so.writeLine("") |
| } |
| } |
| |
| 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.options.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, addNameMap: so.options.shouldGenerateNameMapsFor(metric.Id)}) |
| found = true |
| break |
| } |
| } |
| if !found { |
| enums = append(enums, EnumEntry{ |
| events: events, |
| aliases: md.EventCodeAliases, |
| names: []EnumName{ |
| { |
| prefix: metric.MetricName, |
| suffix: varname, |
| addNameMap: so.options.shouldGenerateNameMapsFor(metric.Id), |
| }, |
| }, |
| }) |
| } |
| } |
| } |
| } |
| |
| 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.options.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, options generatorOptions, outFilename string, errorCalculator *privacy.ErrorCalculator) (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, options), nil |
| case "dart": |
| return DartOutputFactory(varName, options), nil |
| case "go": |
| if goPackageName == "" { |
| return nil, fmt.Errorf("no package name specified for go output formatter") |
| } |
| return GoOutputFactory(varName, goPackageName, options), nil |
| case "java": |
| return JavaOutputFactory(varName, namespaceList, options, outFilename), nil |
| case "json": |
| return JSONOutputFactory(errorCalculator), nil |
| case "rust": |
| return RustOutputFactory(varName, namespaceList, options), 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) |
| } |
| } |