Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add generator package #604

Merged
merged 28 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
29cc0cc
add generator package
reuvenharrison Sep 7, 2024
77a2770
stop using fmt.Fprintln
reuvenharrison Sep 7, 2024
d2de625
remove "become"
reuvenharrison Sep 7, 2024
6f0de06
top level is request body only
reuvenharrison Sep 7, 2024
09876b7
noun -> object
reuvenharrison Sep 7, 2024
cd1ba93
improve readability
reuvenharrison Sep 7, 2024
b42c0b2
generate messages from yaml
reuvenharrison Sep 9, 2024
f6d9ac6
combined add/remove
reuvenharrison Sep 10, 2024
68bba4a
allow multiple objects
reuvenharrison Sep 10, 2024
17d7fb1
fix hierarchy
reuvenharrison Sep 10, 2024
5ecf067
fix hierarchy
reuvenharrison Sep 10, 2024
4076548
add schema
reuvenharrison Sep 10, 2024
14694ea
extend schema
reuvenharrison Sep 10, 2024
13b45cf
references
reuvenharrison Sep 11, 2024
d1480e6
add properties
reuvenharrison Sep 12, 2024
eb51652
rm messages.yaml
reuvenharrison Sep 12, 2024
701a57c
update doc
reuvenharrison Sep 12, 2024
b236aa0
check number of results
reuvenharrison Sep 15, 2024
e2387b3
complete endpoint
reuvenharrison Sep 15, 2024
05b2775
add response
reuvenharrison Sep 15, 2024
f46f755
add endpoint scheme security
reuvenharrison Sep 15, 2024
d3a565b
tree-generated messages
reuvenharrison Sep 16, 2024
3433b09
simplify tree
reuvenharrison Sep 16, 2024
1e422a2
sorted messages
reuvenharrison Sep 16, 2024
d96c882
failed to parse sunset date
reuvenharrison Sep 16, 2024
dea1f0d
fix test
reuvenharrison Sep 16, 2024
a34fa9f
rn data generator
reuvenharrison Sep 16, 2024
2f4e0a6
rm unused funcs
reuvenharrison Sep 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}

Check warning on line 20 in checker/generator/generator.go

View check run for this annotation

Codecov / codecov/patch

checker/generator/generator.go#L19-L20

Added lines #L19 - L20 were not covered by tests

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 ""

Check warning on line 104 in checker/generator/generator.go

View check run for this annotation

Codecov / codecov/patch

checker/generator/generator.go#L103-L104

Added lines #L103 - L104 were not covered by tests
}
}

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)
}

Check warning on line 42 in checker/generator/tree.go

View check run for this annotation

Codecov / codecov/patch

checker/generator/tree.go#L41-L42

Added lines #L41 - L42 were not covered by tests

var changeMap ChangeTree
err = yaml.Unmarshal(yamlFile, &changeMap)
if err != nil {
return nil, fmt.Errorf("unmarshal: %v", err)
}

Check warning on line 48 in checker/generator/tree.go

View check run for this annotation

Codecov / codecov/patch

checker/generator/tree.go#L47-L48

Added lines #L47 - L48 were not covered by tests

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
Loading