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
No related branches found
No related tags found
No related merge requests found
Subproject commit fc23183a86463b2aa81e3b7570fad1f873c1e435 Subproject commit 2e4728f6f3d5d63dcb9d5548b17af9efce605280
...@@ -192,3 +192,15 @@ func CloneSlice[T interface{ Clone() T }](s []T) []T { ...@@ -192,3 +192,15 @@ func CloneSlice[T interface{ Clone() T }](s []T) []T {
} }
return result 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 ...@@ -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 ( ...@@ -18,4 +18,5 @@ var (
ErrSetDefaultLocaleFallback = errors.New("cannot set fallback for default locale") ErrSetDefaultLocaleFallback = errors.New("cannot set fallback for default locale")
ErrDeleteDefaultLocale = errors.New("cannot delete default locale") ErrDeleteDefaultLocale = errors.New("cannot delete default locale")
ErrDeleteFallbackLocale = errors.New("cannot delete locale that is used as fallback for other locales") ErrDeleteFallbackLocale = errors.New("cannot delete locale that is used as fallback for other locales")
ErrCircularDependency = errors.New("fallback locales contain cycles")
) )
package locales package locales
import ( 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" pb "git.perx.ru/perxis/perxis-go/proto/locales"
"golang.org/x/text/language" "golang.org/x/text/language"
) )
...@@ -23,18 +25,18 @@ type Locale struct { ...@@ -23,18 +25,18 @@ type Locale struct {
Disabled bool `json:"disabled" bson:"disabled"` // Запретить использование локали. Нельзя создавать и редактировать контент для данной локали (кроме default) Disabled bool `json:"disabled" bson:"disabled"` // Запретить использование локали. Нельзя создавать и редактировать контент для данной локали (кроме default)
} }
func (l Locale) Clone() *Locale { func (locale *Locale) Clone() *Locale {
return &Locale{ return &Locale{
ID: l.ID, ID: locale.ID,
SpaceID: l.SpaceID, SpaceID: locale.SpaceID,
Name: l.Name, Name: locale.Name,
NativeName: l.NativeName, NativeName: locale.NativeName,
Code: l.Code, Code: locale.Code,
Fallback: l.Fallback, Fallback: locale.Fallback,
Direction: l.Direction, Direction: locale.Direction,
Weight: l.Weight, Weight: locale.Weight,
NoPublish: l.NoPublish, NoPublish: locale.NoPublish,
Disabled: l.Disabled, Disabled: locale.Disabled,
} }
} }
...@@ -46,6 +48,13 @@ func (locale *Locale) Equal(other *Locale) bool { ...@@ -46,6 +48,13 @@ func (locale *Locale) Equal(other *Locale) bool {
return locale == other || locale != nil && other != nil && *locale == *other return locale == other || locale != nil && other != nil && *locale == *other
} }
func (locale *Locale) Key() string {
if locale == nil {
return ""
}
return locale.ID
}
// Возвращает язык локали, например "en", "ru" // Возвращает язык локали, например "en", "ru"
func (locale *Locale) GetLanguage() string { func (locale *Locale) GetLanguage() string {
lang, err := language.Parse(locale.Code) lang, err := language.Parse(locale.Code)
...@@ -102,3 +111,54 @@ func LocaleFromProto(protoLocale *pb.Locale) *Locale { ...@@ -102,3 +111,54 @@ func LocaleFromProto(protoLocale *pb.Locale) *Locale {
} }
return 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 ...@@ -3,6 +3,7 @@ package locales
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
...@@ -79,3 +80,218 @@ func TestLocale_Equal(t *testing.T) { ...@@ -79,3 +80,218 @@ func TestLocale_Equal(t *testing.T) {
require.Equal(t, tt.want, got) 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.
Finish editing this message first!
Please register or to comment