-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1719a2f
commit 95604f1
Showing
9 changed files
with
837 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
/* | ||
Package generator generates the breaking-changes and changelog messages for the checker package. | ||
The output, messages.yaml can be used by the checker package instead of the hardcoded messages under localizations_src. | ||
Advatages over manuallly writing the messages: | ||
- The generated ids and messages are consistent according to the logic in the generator. | ||
- The generator can be easily extended to support more messages. | ||
Additional work needed before using the generator: | ||
- Check that all messages are covered by the generator. | ||
- Decide what to do with Russian messages. | ||
*/ | ||
package generator |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
package generator | ||
|
||
import ( | ||
"slices" | ||
"strings" | ||
|
||
"github.com/iancoleman/strcase" | ||
) | ||
|
||
type MessageGenerator interface { | ||
generate() []string | ||
} | ||
|
||
type Getter func() (MessageGenerator, error) | ||
|
||
func Generate(getter Getter) ([]string, error) { | ||
data, err := getter() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return data.generate(), nil | ||
} | ||
|
||
func isEmpty(s string) bool { | ||
return s == "" | ||
} | ||
|
||
func filterStrings(list []string, f func(string) bool) []string { | ||
var result []string | ||
for _, s := range list { | ||
if !f(s) { | ||
result = append(result, s) | ||
} | ||
} | ||
return result | ||
} | ||
|
||
func generateId(hierarchy []string, object, action, adverb string) string { | ||
if prefix, _, found := strings.Cut(object, "/"); found { | ||
object = prefix | ||
} | ||
|
||
return strcase.ToKebab(strings.Join(filterStrings([]string{concat(hierarchy), object, conjugate(action), adverb}, isEmpty), "-")) | ||
} | ||
|
||
func concat(list []string) string { | ||
if len(list) == 0 { | ||
return "" | ||
} | ||
|
||
copy := slices.Clone(list) | ||
slices.Reverse(copy) | ||
return strings.Join(copy, "-") | ||
} | ||
|
||
func getHierarchyPostfix(action string, hierarchy []string) string { | ||
if len(hierarchy) == 0 { | ||
return "" | ||
} | ||
|
||
return getPreposition(action) + " " + getHierarchyMessage(hierarchy) | ||
} | ||
|
||
func getHierarchyMessage(hierarchy []string) string { | ||
|
||
copy := slices.Clone(hierarchy) | ||
|
||
for i, s := range hierarchy { | ||
if isAtttibuted(s) { | ||
copy[i] = "%s " + s | ||
} | ||
} | ||
result := strings.Join(copy, " %s of ") | ||
|
||
if hierarchy != nil && !isTopLevel(hierarchy[len(hierarchy)-1]) { | ||
result += " %s" | ||
} | ||
|
||
return result | ||
} | ||
|
||
func isTopLevel(s string) bool { | ||
return s == "request body" | ||
} | ||
|
||
func isAtttibuted(s string) bool { | ||
return s == "request parameter" | ||
} | ||
|
||
func standardizeSpaces(s string) string { | ||
return strings.Join(strings.Fields(s), " ") | ||
} | ||
|
||
func getActionMessage(action string) string { | ||
switch getArity(action) { | ||
case 0: | ||
return "" | ||
case 1: | ||
return " to %s" | ||
case 2: | ||
return " from %s to %s" | ||
default: | ||
return "" | ||
} | ||
} | ||
|
||
func getArity(action string) int { | ||
switch action { | ||
case "add", "remove": | ||
return 0 | ||
case "set": | ||
return 1 | ||
} | ||
return 2 | ||
} | ||
|
||
func conjugate(verb string) string { | ||
switch verb { | ||
case "set": | ||
return "set" | ||
case "add": | ||
return "added" | ||
case "fail to parse": | ||
return "failed to parse" | ||
} | ||
return verb + "d" | ||
} | ||
|
||
func getPreposition(action string) string { | ||
switch action { | ||
case "add": | ||
return "to" | ||
} | ||
return "from" | ||
} | ||
|
||
func addAttribute(name, attributiveAdjective, predicativeAdjective string) string { | ||
return strings.Join([]string{attributiveAdjective + " " + name + " " + predicativeAdjective}, " ") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package generator_test | ||
|
||
import ( | ||
"os" | ||
"slices" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
"github.com/tufin/oasdiff/checker/generator" | ||
) | ||
|
||
func WriteToFile(t *testing.T, filename string, lines []string) { | ||
t.Helper() | ||
|
||
file, err := os.Create(filename) | ||
require.NoError(t, err) | ||
defer file.Close() | ||
for _, line := range lines { | ||
_, err = file.WriteString(line + "\n") | ||
require.NoError(t, err) | ||
} | ||
} | ||
|
||
func TestTreeGenerator(t *testing.T) { | ||
result, err := generator.Generate(generator.GetTree("tree.yaml")) | ||
require.NoError(t, err) | ||
slices.Sort(result) | ||
WriteToFile(t, "messages.yaml", result) | ||
require.Len(t, result, 263) | ||
badId, unique := isUninueIds(result) | ||
require.True(t, unique, badId) | ||
} | ||
|
||
func isUninueIds(messages []string) (string, bool) { | ||
ids := make(map[string]struct{}) | ||
for _, message := range messages { | ||
id := strings.SplitAfter(message, ":")[0] | ||
if _, ok := ids[id]; ok { | ||
return id, false | ||
} | ||
ids[id] = struct{}{} | ||
} | ||
return "", true | ||
} |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
package generator | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"strings" | ||
|
||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
type ChangeTree struct { | ||
Changes ChangeMap `yaml:"changes"` | ||
Components ChangeMap `yaml:"components"` | ||
} | ||
|
||
type ChangeMap map[string]Changes | ||
|
||
type Changes struct { | ||
Ref string `yaml:"$ref"` | ||
ExcludeFromHierarchy bool `yaml:"excludeFromHierarchy"` | ||
Actions Actions `yaml:"actions"` | ||
NextLevel ChangeMap `yaml:"nextLevel"` | ||
} | ||
|
||
type Actions map[string]Objects | ||
type Objects []Object | ||
|
||
type Object struct { | ||
Hierarchy []string `yaml:"hierarchy"` | ||
Names []string `yaml:"names"` | ||
Adverbs []string `yaml:"adverbs"` | ||
StartWithName bool `yaml:"startWithName"` | ||
PredicativeAdjective string `yaml:"predicativeAdjective"` | ||
AttributiveAdjective string `yaml:"attributiveAdjective"` | ||
} | ||
|
||
func GetTree(file string) func() (MessageGenerator, error) { | ||
return func() (MessageGenerator, error) { | ||
yamlFile, err := os.ReadFile(file) | ||
if err != nil { | ||
return nil, fmt.Errorf("yamlFile.Get err #%v ", err) | ||
} | ||
|
||
var changeMap ChangeTree | ||
err = yaml.Unmarshal(yamlFile, &changeMap) | ||
if err != nil { | ||
return nil, fmt.Errorf("unmarshal: %v", err) | ||
} | ||
|
||
return changeMap, nil | ||
} | ||
} | ||
|
||
func (changeTree ChangeTree) generate() []string { | ||
resolveRefs(changeTree.Changes, changeTree.Components) | ||
fillHierarchy(changeTree.Changes, nil) | ||
return generateRecursive(changeTree.Changes) | ||
} | ||
|
||
func (changeMap ChangeMap) copy() ChangeMap { | ||
result := ChangeMap{} | ||
for key, value := range changeMap { | ||
result[key] = value.copy() | ||
} | ||
return result | ||
} | ||
|
||
func (changes Changes) copy() Changes { | ||
return Changes{ | ||
Ref: changes.Ref, | ||
ExcludeFromHierarchy: changes.ExcludeFromHierarchy, | ||
Actions: changes.Actions.copy(), | ||
NextLevel: changes.NextLevel.copy(), | ||
} | ||
} | ||
|
||
func (actions Actions) copy() Actions { | ||
result := Actions{} | ||
for key, value := range actions { | ||
result[key] = value.copy() | ||
} | ||
return result | ||
} | ||
|
||
func (objects Objects) copy() Objects { | ||
result := make(Objects, 0, len(objects)) | ||
return append(result, objects...) | ||
} | ||
|
||
func resolveRefs(changes ChangeMap, components ChangeMap) { | ||
for container, change := range changes { | ||
if change.Ref != "" { | ||
changes[container] = components[change.Ref].copy() | ||
} | ||
resolveRefs(changes[container].NextLevel, components) | ||
} | ||
} | ||
|
||
func generateRecursive(changes ChangeMap) []string { | ||
result := []string{} | ||
|
||
for _, change := range changes { | ||
for action, objects := range change.Actions { | ||
for _, object := range objects { | ||
result = append(result, getValueSet(object, action).generate()...) | ||
} | ||
} | ||
result = append(result, generateRecursive(change.NextLevel)...) | ||
} | ||
|
||
return result | ||
} | ||
|
||
func fillHierarchy(changes ChangeMap, hierarchy []string) { | ||
for container, change := range changes { | ||
containerHierarchy := getContainerHierarchy(container, change, hierarchy) | ||
for action, objects := range change.Actions { | ||
for i := range objects { | ||
changes[container].Actions[action][i].Hierarchy = containerHierarchy | ||
} | ||
} | ||
fillHierarchy(change.NextLevel, containerHierarchy) | ||
} | ||
} | ||
|
||
func getContainerHierarchy(container string, change Changes, hierarchy []string) []string { | ||
if change.ExcludeFromHierarchy { | ||
return hierarchy | ||
} | ||
return append([]string{container}, hierarchy...) | ||
} | ||
|
||
func getValueSet(object Object, action string) IValueSet { | ||
valueSet := ValueSet{ | ||
AttributiveAdjective: object.AttributiveAdjective, | ||
PredicativeAdjective: object.PredicativeAdjective, | ||
Hierarchy: object.Hierarchy, | ||
Names: object.Names, | ||
Actions: parseAction(action), | ||
Adverbs: object.Adverbs, | ||
} | ||
|
||
if object.StartWithName { | ||
return ValueSetA(valueSet) | ||
} | ||
return ValueSetB(valueSet) | ||
} | ||
|
||
func parseAction(action string) []string { | ||
return strings.Split(action, "/") | ||
} |
Oops, something went wrong.