From a8785aa55abe86d82bba4fe489cac5bdfeef9bca Mon Sep 17 00:00:00 2001 From: Benjamin Sanabria Carr Date: Thu, 18 Apr 2024 23:28:38 -0700 Subject: [PATCH] semconvgen: Add a flag for a path to a custom capitalizations file (#529) --- ...nvgen-flag-for-custom-capitalizations.yaml | 16 ++++ semconvgen/README.md | 21 ++--- semconvgen/generator.go | 59 +++++++++++-- semconvgen/generator_test.go | 83 +++++++++++++++++++ 4 files changed, 160 insertions(+), 19 deletions(-) create mode 100644 .chloggen/528-semconvgen-flag-for-custom-capitalizations.yaml create mode 100644 semconvgen/generator_test.go diff --git a/.chloggen/528-semconvgen-flag-for-custom-capitalizations.yaml b/.chloggen/528-semconvgen-flag-for-custom-capitalizations.yaml new file mode 100644 index 00000000..6aa93bf5 --- /dev/null +++ b/.chloggen/528-semconvgen-flag-for-custom-capitalizations.yaml @@ -0,0 +1,16 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. crosslink) +component: semconvgen + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: "Add `--capitalizations-path` to allow users to add additional strings to the static capitalizations slice in generator.go" + +# One or more tracking issues related to the change +issues: [528] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: diff --git a/semconvgen/README.md b/semconvgen/README.md index 3e422fb0..645dae84 100644 --- a/semconvgen/README.md +++ b/semconvgen/README.md @@ -22,13 +22,14 @@ semconvgen -i -t -o A full list of available options: -```txt - -c, --container string Container image ID (default "otel/semconvgen") - -f, --filename string Filename for templated output. If not specified 'basename(inputPath).go' will be used. - -i, --input string Path to semantic convention definition YAML. Should be a directory in the specification git repository. - --only string Process only semantic conventions of the specified type. {span, resource, event, metric_group, metric, units, scope, attribute_group} - -o, --output string Path to output target. Must be either an absolute path or relative to the repository root. If unspecified will output to a sub-directory with the name matching the version number specified via --specver flag. - -p, --parameters string List of key=value pairs separated by comma. These values are fed into the template as-is. - -s, --specver string Version of semantic convention to generate. Must be an existing version tag in the specification git repository. - -t, --template string Template filename (default "template.j2") -``` +````txt + -z, --capitalizations-path string Path to a file containing additional newline-separated capitalization strings. + -c, --container string Container image ID (default "otel/semconvgen") + -f, --filename string Filename for templated output. If not specified 'basename(inputPath).go' will be used. + -i, --input string Path to semantic convention definition YAML. Should be a directory in the specification git repository. + --only string Process only semantic conventions of the specified type. {span, resource, event, metric_group, metric, units, scope, attribute_group} + -o, --output string Path to output target. Must be either an absolute path or relative to the repository root. If unspecified will output to a sub-directory with the name matching the version number specified via --specver flag. + -p, --parameters string List of key=value pairs separated by comma. These values are fed into the template as-is. + -s, --specver string Version of semantic convention to generate. Must be an existing version tag in the specification git repository. + -t, --template string Template filename (default "template.j2")``` +```` diff --git a/semconvgen/generator.go b/semconvgen/generator.go index ac1ac861..4ff1c656 100644 --- a/semconvgen/generator.go +++ b/semconvgen/generator.go @@ -15,6 +15,7 @@ package main import ( + "bufio" "bytes" "errors" "fmt" @@ -48,6 +49,7 @@ func main() { flag.StringVarP(&cfg.outputFilename, "filename", "f", "", "Filename for templated output. If not specified 'basename(inputPath).go' will be used.") flag.StringVarP(&cfg.templateFilename, "template", "t", "template.j2", "Template filename") flag.StringVarP(&cfg.templateParameters, "parameters", "p", "", "List of key=value pairs separated by comma. These values are fed into the template as-is.") + flag.StringVarP(&cfg.capitalizationsPath, "capitalizations-path", "z", "", "Path to a file containing additional newline-separated capitalization strings.") flag.Parse() cfg, err := validateConfig(cfg) @@ -74,14 +76,15 @@ func main() { } type config struct { - inputPath string - outputPath string - outputFilename string - templateFilename string - templateParameters string - onlyType string - containerImage string - specVersion string + inputPath string + outputPath string + outputFilename string + templateFilename string + templateParameters string + onlyType string + containerImage string + specVersion string + capitalizationsPath string } func validateConfig(cfg config) (config, error) { @@ -126,6 +129,12 @@ func validateConfig(cfg config) (config, error) { cfg.templateFilename = path.Join(pwd, cfg.templateFilename) } + if cfg.capitalizationsPath != "" { + if _, err := os.Stat(cfg.capitalizationsPath); os.IsNotExist(err) { + return config{}, fmt.Errorf("capitalizations file does not exist: %w", err) + } + } + return cfg, nil } @@ -282,7 +291,7 @@ func checkoutSpecToDir(cfg config, toDir string) (doneFunc func(), err error) { return doneFunc, nil } -var capitalizations = []string{ +var staticCapitalizations = []string{ "ACL", "AIX", "AKS", @@ -379,6 +388,34 @@ var capitalizations = []string{ "OTel", } +func capitalizations(capitalizationsPath string) ([]string, error) { + c := append([]string(nil), staticCapitalizations...) + + if capitalizationsPath != "" { + // #nosec G304 -- We expect the file path to be provided by the user. + file, err := os.Open(capitalizationsPath) + if err != nil { + return nil, fmt.Errorf("unable to open capitalizations file: %w", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + capitalization := strings.TrimSpace(scanner.Text()) + if capitalization == "" { + continue + } + c = append(c, capitalization) + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("unable to read capitalizations file: %w", err) + } + } + + return c, nil +} + // These are not simple capitalization fixes, but require string replacement. // All occurrences of the key will be replaced with the corresponding value. var replacements = map[string]string{ @@ -394,6 +431,10 @@ func fixIdentifiers(cfg config) error { return fmt.Errorf("unable to read file: %w", err) } + capitalizations, err := capitalizations(cfg.capitalizationsPath) + if err != nil { + return fmt.Errorf("unable to combine custom and static capitalizations: %w", err) + } caser := cases.Title(language.Und) for _, init := range capitalizations { // Match the title-cased capitalization target, asserting that its followed by diff --git a/semconvgen/generator_test.go b/semconvgen/generator_test.go new file mode 100644 index 00000000..cfa1cd45 --- /dev/null +++ b/semconvgen/generator_test.go @@ -0,0 +1,83 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "os" + "reflect" + "testing" +) + +func TestCapitalizations(t *testing.T) { + tests := []struct { + name string + capitalizations string + want []string + }{ + { + name: "No additional capitalizations", + capitalizations: "", + want: staticCapitalizations, + }, + { + name: "Some additional capitalizations", + capitalizations: "ASPNETCore\nJVM", + want: append(staticCapitalizations, "ASPNETCore", "JVM"), + }, + { + name: "Wrong separator for capitalizations", + capitalizations: "ASPNETCore,JVM", + want: append(staticCapitalizations, "ASPNETCore,JVM"), + }, + { + name: "Copius amounts of whitespace", + capitalizations: ` + + ASPNETCore + + JVM + + + `, + want: append(staticCapitalizations, "ASPNETCore", "JVM"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpfile, err := os.CreateTemp("", "test") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) + + if _, err = tmpfile.Write([]byte(tt.capitalizations)); err != nil { + t.Fatal(err) + } + if err = tmpfile.Close(); err != nil { + t.Fatal(err) + } + + customCapitalizations, err := capitalizations(tmpfile.Name()) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(customCapitalizations, tt.want) { + t.Errorf("capitalizations() = %v, want %v", customCapitalizations, tt.want) + } + }) + } +}