| // 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, 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]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, c.Customers[0].Projects[0].ProjectName, enums) |
| } |
| |
| 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) |
| } |
| } |