Skip to content
Snippets Groups Projects
Commit 179d3bb8 authored by Semyon Krestyaninov's avatar Semyon Krestyaninov :dog2: Committed by Pavel Antonov
Browse files

Добавлена топологическая сортировка локалей

parent aba766b3
Branches
Tags
No related merge requests found
Subproject commit fc23183a86463b2aa81e3b7570fad1f873c1e435
Subproject commit 2e4728f6f3d5d63dcb9d5548b17af9efce605280
......@@ -192,3 +192,15 @@ func CloneSlice[T interface{ Clone() T }](s []T) []T {
}
return result
}
type Keyed[T comparable] interface {
Key() T
}
func SliceToMap[K comparable, V Keyed[K]](s []V) map[K]V {
res := make(map[K]V, len(s))
for _, elem := range s {
res[elem.Key()] = elem
}
return res
}
......@@ -139,3 +139,18 @@ keyB2: val20
})
}
}
type KV struct {
K string
V string
}
func (kv *KV) Key() string {
return kv.K
}
func TestSliceToMap(t *testing.T) {
s := []*KV{{"a", "1"}, {"b", "2"}, {"c", "3"}}
m := SliceToMap(s)
assert.Equal(t, map[string]*KV{"a": {"a", "1"}, "b": {"b", "2"}, "c": {"c", "3"}}, m)
}
......@@ -18,4 +18,5 @@ var (
ErrSetDefaultLocaleFallback = errors.New("cannot set fallback for default locale")
ErrDeleteDefaultLocale = errors.New("cannot delete default locale")
ErrDeleteFallbackLocale = errors.New("cannot delete locale that is used as fallback for other locales")
ErrCircularDependency = errors.New("fallback locales contain cycles")
)
package locales
import (
"git.perx.ru/perxis/perxis-go/pkg/data"
"git.perx.ru/perxis/perxis-go/pkg/errors"
pb "git.perx.ru/perxis/perxis-go/proto/locales"
"golang.org/x/text/language"
)
......@@ -23,18 +25,18 @@ type Locale struct {
Disabled bool `json:"disabled" bson:"disabled"` // Запретить использование локали. Нельзя создавать и редактировать контент для данной локали (кроме default)
}
func (l Locale) Clone() *Locale {
func (locale *Locale) Clone() *Locale {
return &Locale{
ID: l.ID,
SpaceID: l.SpaceID,
Name: l.Name,
NativeName: l.NativeName,
Code: l.Code,
Fallback: l.Fallback,
Direction: l.Direction,
Weight: l.Weight,
NoPublish: l.NoPublish,
Disabled: l.Disabled,
ID: locale.ID,
SpaceID: locale.SpaceID,
Name: locale.Name,
NativeName: locale.NativeName,
Code: locale.Code,
Fallback: locale.Fallback,
Direction: locale.Direction,
Weight: locale.Weight,
NoPublish: locale.NoPublish,
Disabled: locale.Disabled,
}
}
......@@ -46,6 +48,13 @@ func (locale *Locale) Equal(other *Locale) bool {
return locale == other || locale != nil && other != nil && *locale == *other
}
func (locale *Locale) Key() string {
if locale == nil {
return ""
}
return locale.ID
}
// Возвращает язык локали, например "en", "ru"
func (locale *Locale) GetLanguage() string {
lang, err := language.Parse(locale.Code)
......@@ -102,3 +111,54 @@ func LocaleFromProto(protoLocale *pb.Locale) *Locale {
}
return locale
}
func FallbackSort(locales []*Locale) ([]*Locale, error) {
if len(locales) == 0 {
return locales, nil
}
var (
unsorted = data.SliceToMap(locales)
sorted = make([]*Locale, 0, len(locales))
visited = make(map[string]bool, len(locales))
)
for {
changed := false
for _, node := range locales {
if visited[node.ID] {
continue
}
if _, exist := unsorted[node.Fallback]; !exist && node.Fallback != "" {
return nil, errors.Wrap(ErrNotFound, node.Fallback)
}
if node.IsDefault() {
sorted = append([]*Locale{node}, sorted...)
visited[node.ID] = true
changed = true
continue
}
if node.Fallback == "" || visited[node.Fallback] {
sorted = append(sorted, node)
visited[node.ID] = true
changed = true
}
}
// Все локали перенесены в список отсортированных
if len(sorted) == len(locales) {
break
}
// Если не произошло изменений, но не все локали отсортированы - обнаружены циклические зависимости
if !changed {
return nil, ErrCircularDependency
}
}
return sorted, nil
}
......@@ -3,6 +3,7 @@ package locales
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
......@@ -79,3 +80,218 @@ func TestLocale_Equal(t *testing.T) {
require.Equal(t, tt.want, got)
}
}
func TestFallbackSort(t *testing.T) {
tests := []struct {
name string
input []*Locale
want []*Locale
wantErr bool
}{
{
name: "Empty input",
input: nil,
want: nil,
wantErr: false,
},
{
name: "Empty input",
input: []*Locale{},
want: []*Locale{},
wantErr: false,
},
{
name: "One locale not default",
input: []*Locale{
{ID: "a"},
},
want: []*Locale{
{ID: "a"},
},
wantErr: false,
},
{
name: "One default locale",
input: []*Locale{
{ID: DefaultID},
},
want: []*Locale{
{ID: DefaultID},
},
wantErr: false,
},
{
name: "One locale with fallback",
input: []*Locale{
{ID: DefaultID, Fallback: "a"},
},
wantErr: true,
},
{
name: "Fallback locale not exists",
input: []*Locale{
{ID: "a", Fallback: "c"},
{ID: "b", Fallback: DefaultID},
},
want: nil,
wantErr: true,
},
{
name: "Not found default",
input: []*Locale{
{ID: "a", Fallback: DefaultID},
{ID: "b", Fallback: "d"},
{ID: "c", Fallback: DefaultID},
{ID: "d", Fallback: "c"},
{ID: "e", Fallback: "a"},
},
wantErr: true,
},
{
name: "Simple",
input: []*Locale{
{ID: DefaultID},
{ID: "a", Fallback: DefaultID},
{ID: "b", Fallback: "d"},
{ID: "c", Fallback: DefaultID},
{ID: "d", Fallback: "c"},
{ID: "e", Fallback: "a"},
},
want: []*Locale{
{ID: DefaultID},
{ID: "a", Fallback: DefaultID},
{ID: "c", Fallback: DefaultID},
{ID: "d", Fallback: "c"},
{ID: "e", Fallback: "a"},
{ID: "b", Fallback: "d"},
},
wantErr: false,
},
{
name: "Simple #2",
input: []*Locale{
{ID: "a", Fallback: DefaultID},
{ID: "b", Fallback: "d"},
{ID: "c", Fallback: DefaultID},
{ID: "d", Fallback: "c"},
{ID: "e", Fallback: "a"},
{ID: DefaultID},
},
want: []*Locale{
{ID: DefaultID},
{ID: "a", Fallback: DefaultID},
{ID: "c", Fallback: DefaultID},
{ID: "d", Fallback: "c"},
{ID: "e", Fallback: "a"},
{ID: "b", Fallback: "d"},
},
wantErr: false,
},
{
name: "Simple #3",
input: []*Locale{
{ID: DefaultID},
{ID: "a"},
{ID: "b"},
},
want: []*Locale{
{ID: DefaultID},
{ID: "a"},
{ID: "b"},
},
wantErr: false,
},
{
name: "Simple #4",
input: []*Locale{
{ID: "a"},
{ID: "b"},
{ID: DefaultID},
},
want: []*Locale{
{ID: DefaultID},
{ID: "a"},
{ID: "b"},
},
wantErr: false,
},
{
name: "Simple #5",
input: []*Locale{
{ID: DefaultID},
{ID: "a", Fallback: "b"},
{ID: "b", Fallback: "c"},
{ID: "c"},
},
want: []*Locale{
{ID: DefaultID},
{ID: "c"},
{ID: "b", Fallback: "c"},
{ID: "a", Fallback: "b"},
},
wantErr: false,
},
{
name: "Simple #6",
input: []*Locale{
{ID: "a", Fallback: "b"},
{ID: "b", Fallback: "c"},
{ID: "c"},
},
want: []*Locale{
{ID: "c"},
{ID: "b", Fallback: "c"},
{ID: "a", Fallback: "b"},
},
wantErr: false,
},
{
name: "Cyclical dependency",
input: []*Locale{
{ID: DefaultID},
{ID: "a", Fallback: "b"},
{ID: "b", Fallback: "c"},
{ID: "c", Fallback: "a"},
},
want: nil,
wantErr: true,
},
{
name: "Cyclical dependency #2",
input: []*Locale{
{ID: DefaultID},
{ID: "a", Fallback: DefaultID},
{ID: "b", Fallback: "d"},
{ID: "c", Fallback: "b"},
{ID: "d", Fallback: "c"},
{ID: "e", Fallback: "a"},
},
want: nil,
wantErr: true,
},
{
name: "Cyclical dependency #3",
input: []*Locale{
{ID: DefaultID},
{ID: "a", Fallback: "b"},
{ID: "b", Fallback: "a"},
{ID: "c", Fallback: "d"},
{ID: "d", Fallback: "c"},
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := FallbackSort(tt.input)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment