Skip to content

Commit

Permalink
add generator package (#604)
Browse files Browse the repository at this point in the history
  • Loading branch information
reuvenharrison authored Sep 16, 2024
1 parent 1719a2f commit 95604f1
Show file tree
Hide file tree
Showing 9 changed files with 837 additions and 0 deletions.
11 changes: 11 additions & 0 deletions checker/generator/doc.go
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
140 changes: 140 additions & 0 deletions checker/generator/generator.go
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}, " ")
}
45 changes: 45 additions & 0 deletions checker/generator/generator_test.go
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
}
263 changes: 263 additions & 0 deletions checker/generator/messages.yaml

Large diffs are not rendered by default.

151 changes: 151 additions & 0 deletions checker/generator/tree.go
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, "/")
}
Loading

0 comments on commit 95604f1

Please sign in to comment.