Skip to content
Snippets Groups Projects
Commit c2bfdd05 authored by Pavel Antonov's avatar Pavel Antonov :asterisk:
Browse files

Merge branch 'feature/PRXS-2648-LocalizedSync' into 'master'

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

See merge request perxis/perxis-go!291
parents aba766b3 179d3bb8
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