diff --git a/contracts/support/pluralizer/inflector.go b/contracts/support/pluralizer/inflector.go new file mode 100644 index 000000000..872c4599a --- /dev/null +++ b/contracts/support/pluralizer/inflector.go @@ -0,0 +1,8 @@ +package pluralizer + +type Inflector interface { + Language() Language + Plural(word string) string + SetLanguage(language Language) Inflector + Singular(word string) string +} diff --git a/contracts/support/pluralizer/language.go b/contracts/support/pluralizer/language.go new file mode 100644 index 000000000..1511fa75f --- /dev/null +++ b/contracts/support/pluralizer/language.go @@ -0,0 +1,26 @@ +package pluralizer + +type Language interface { + Name() string + SingularRuleset() Ruleset + PluralRuleset() Ruleset +} + +type Transformation interface { + Apply(word string) string +} + +type Pattern interface { + Matches(word string) bool +} + +type Substitution interface { + From() string + To() string +} + +type Transformations []Transformation + +type Patterns []Pattern + +type Substitutions []Substitution diff --git a/contracts/support/pluralizer/rules.go b/contracts/support/pluralizer/rules.go new file mode 100644 index 000000000..b63c58b35 --- /dev/null +++ b/contracts/support/pluralizer/rules.go @@ -0,0 +1,9 @@ +package pluralizer + +type Ruleset interface { + AddIrregular(substitutions ...Substitution) Ruleset + AddUninflected(words ...string) Ruleset + Regular() Transformations + Uninflected() Patterns + Irregular() Substitutions +} diff --git a/errors/list.go b/errors/list.go index 7e4461832..03cdd5003 100644 --- a/errors/list.go +++ b/errors/list.go @@ -136,6 +136,11 @@ var ( PackageRegistrationDuplicate = New("'%s' had been registered") PackageRegistrationNotFound = New("'%s' not found, cannot insert before it") + PluralizerLanguageNotFound = New("language %s not found").SetModule(ModulePluralizer) + PluralizerEmptyLanguageName = New("language name cannot be empty").SetModule(ModulePluralizer) + PluralizerNoSubstitutionsGiven = New("no substitutions provided").SetModule(ModulePluralizer) + PluralizerNoWordsGiven = New("no words provided").SetModule(ModulePluralizer) + QueueDriverFailedToPop = New("failed to pop job from %s queue: %v") QueueDriverInvalid = New("%s doesn't implement contracts/queue/driver") QueueDriverNoJobFound = New("no job found in %s queue") diff --git a/errors/modules.go b/errors/modules.go index 7c7a02029..4503f39d9 100644 --- a/errors/modules.go +++ b/errors/modules.go @@ -18,6 +18,7 @@ var ( ModuleMigration = "migration" ModuleOrm = "orm" ModulePackages = "packages" + ModulePluralizer = "pluralizer" ModuleQueue = "queue" ModuleRoute = "route" ModuleSchema = "schema" diff --git a/mocks/support/pluralizer/Inflector.go b/mocks/support/pluralizer/Inflector.go new file mode 100644 index 000000000..7ff63e68b --- /dev/null +++ b/mocks/support/pluralizer/Inflector.go @@ -0,0 +1,222 @@ +// Code generated by mockery. DO NOT EDIT. + +package pluralizer + +import ( + pluralizer "github.com/goravel/framework/contracts/support/pluralizer" + mock "github.com/stretchr/testify/mock" +) + +// Inflector is an autogenerated mock type for the Inflector type +type Inflector struct { + mock.Mock +} + +type Inflector_Expecter struct { + mock *mock.Mock +} + +func (_m *Inflector) EXPECT() *Inflector_Expecter { + return &Inflector_Expecter{mock: &_m.Mock} +} + +// Language provides a mock function with no fields +func (_m *Inflector) Language() pluralizer.Language { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Language") + } + + var r0 pluralizer.Language + if rf, ok := ret.Get(0).(func() pluralizer.Language); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pluralizer.Language) + } + } + + return r0 +} + +// Inflector_Language_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Language' +type Inflector_Language_Call struct { + *mock.Call +} + +// Language is a helper method to define mock.On call +func (_e *Inflector_Expecter) Language() *Inflector_Language_Call { + return &Inflector_Language_Call{Call: _e.mock.On("Language")} +} + +func (_c *Inflector_Language_Call) Run(run func()) *Inflector_Language_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Inflector_Language_Call) Return(_a0 pluralizer.Language) *Inflector_Language_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Inflector_Language_Call) RunAndReturn(run func() pluralizer.Language) *Inflector_Language_Call { + _c.Call.Return(run) + return _c +} + +// Plural provides a mock function with given fields: word +func (_m *Inflector) Plural(word string) string { + ret := _m.Called(word) + + if len(ret) == 0 { + panic("no return value specified for Plural") + } + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(word) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Inflector_Plural_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Plural' +type Inflector_Plural_Call struct { + *mock.Call +} + +// Plural is a helper method to define mock.On call +// - word string +func (_e *Inflector_Expecter) Plural(word interface{}) *Inflector_Plural_Call { + return &Inflector_Plural_Call{Call: _e.mock.On("Plural", word)} +} + +func (_c *Inflector_Plural_Call) Run(run func(word string)) *Inflector_Plural_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Inflector_Plural_Call) Return(_a0 string) *Inflector_Plural_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Inflector_Plural_Call) RunAndReturn(run func(string) string) *Inflector_Plural_Call { + _c.Call.Return(run) + return _c +} + +// SetLanguage provides a mock function with given fields: language +func (_m *Inflector) SetLanguage(language pluralizer.Language) pluralizer.Inflector { + ret := _m.Called(language) + + if len(ret) == 0 { + panic("no return value specified for SetLanguage") + } + + var r0 pluralizer.Inflector + if rf, ok := ret.Get(0).(func(pluralizer.Language) pluralizer.Inflector); ok { + r0 = rf(language) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pluralizer.Inflector) + } + } + + return r0 +} + +// Inflector_SetLanguage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetLanguage' +type Inflector_SetLanguage_Call struct { + *mock.Call +} + +// SetLanguage is a helper method to define mock.On call +// - language pluralizer.Language +func (_e *Inflector_Expecter) SetLanguage(language interface{}) *Inflector_SetLanguage_Call { + return &Inflector_SetLanguage_Call{Call: _e.mock.On("SetLanguage", language)} +} + +func (_c *Inflector_SetLanguage_Call) Run(run func(language pluralizer.Language)) *Inflector_SetLanguage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(pluralizer.Language)) + }) + return _c +} + +func (_c *Inflector_SetLanguage_Call) Return(_a0 pluralizer.Inflector) *Inflector_SetLanguage_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Inflector_SetLanguage_Call) RunAndReturn(run func(pluralizer.Language) pluralizer.Inflector) *Inflector_SetLanguage_Call { + _c.Call.Return(run) + return _c +} + +// Singular provides a mock function with given fields: word +func (_m *Inflector) Singular(word string) string { + ret := _m.Called(word) + + if len(ret) == 0 { + panic("no return value specified for Singular") + } + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(word) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Inflector_Singular_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Singular' +type Inflector_Singular_Call struct { + *mock.Call +} + +// Singular is a helper method to define mock.On call +// - word string +func (_e *Inflector_Expecter) Singular(word interface{}) *Inflector_Singular_Call { + return &Inflector_Singular_Call{Call: _e.mock.On("Singular", word)} +} + +func (_c *Inflector_Singular_Call) Run(run func(word string)) *Inflector_Singular_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Inflector_Singular_Call) Return(_a0 string) *Inflector_Singular_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Inflector_Singular_Call) RunAndReturn(run func(string) string) *Inflector_Singular_Call { + _c.Call.Return(run) + return _c +} + +// NewInflector creates a new instance of Inflector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewInflector(t interface { + mock.TestingT + Cleanup(func()) +}) *Inflector { + mock := &Inflector{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/support/pluralizer/Language.go b/mocks/support/pluralizer/Language.go new file mode 100644 index 000000000..807c0b8e7 --- /dev/null +++ b/mocks/support/pluralizer/Language.go @@ -0,0 +1,174 @@ +// Code generated by mockery. DO NOT EDIT. + +package pluralizer + +import ( + pluralizer "github.com/goravel/framework/contracts/support/pluralizer" + mock "github.com/stretchr/testify/mock" +) + +// Language is an autogenerated mock type for the Language type +type Language struct { + mock.Mock +} + +type Language_Expecter struct { + mock *mock.Mock +} + +func (_m *Language) EXPECT() *Language_Expecter { + return &Language_Expecter{mock: &_m.Mock} +} + +// Name provides a mock function with no fields +func (_m *Language) Name() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Name") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Language_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name' +type Language_Name_Call struct { + *mock.Call +} + +// Name is a helper method to define mock.On call +func (_e *Language_Expecter) Name() *Language_Name_Call { + return &Language_Name_Call{Call: _e.mock.On("Name")} +} + +func (_c *Language_Name_Call) Run(run func()) *Language_Name_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Language_Name_Call) Return(_a0 string) *Language_Name_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Language_Name_Call) RunAndReturn(run func() string) *Language_Name_Call { + _c.Call.Return(run) + return _c +} + +// PluralRuleset provides a mock function with no fields +func (_m *Language) PluralRuleset() pluralizer.Ruleset { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for PluralRuleset") + } + + var r0 pluralizer.Ruleset + if rf, ok := ret.Get(0).(func() pluralizer.Ruleset); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pluralizer.Ruleset) + } + } + + return r0 +} + +// Language_PluralRuleset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PluralRuleset' +type Language_PluralRuleset_Call struct { + *mock.Call +} + +// PluralRuleset is a helper method to define mock.On call +func (_e *Language_Expecter) PluralRuleset() *Language_PluralRuleset_Call { + return &Language_PluralRuleset_Call{Call: _e.mock.On("PluralRuleset")} +} + +func (_c *Language_PluralRuleset_Call) Run(run func()) *Language_PluralRuleset_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Language_PluralRuleset_Call) Return(_a0 pluralizer.Ruleset) *Language_PluralRuleset_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Language_PluralRuleset_Call) RunAndReturn(run func() pluralizer.Ruleset) *Language_PluralRuleset_Call { + _c.Call.Return(run) + return _c +} + +// SingularRuleset provides a mock function with no fields +func (_m *Language) SingularRuleset() pluralizer.Ruleset { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for SingularRuleset") + } + + var r0 pluralizer.Ruleset + if rf, ok := ret.Get(0).(func() pluralizer.Ruleset); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pluralizer.Ruleset) + } + } + + return r0 +} + +// Language_SingularRuleset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SingularRuleset' +type Language_SingularRuleset_Call struct { + *mock.Call +} + +// SingularRuleset is a helper method to define mock.On call +func (_e *Language_Expecter) SingularRuleset() *Language_SingularRuleset_Call { + return &Language_SingularRuleset_Call{Call: _e.mock.On("SingularRuleset")} +} + +func (_c *Language_SingularRuleset_Call) Run(run func()) *Language_SingularRuleset_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Language_SingularRuleset_Call) Return(_a0 pluralizer.Ruleset) *Language_SingularRuleset_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Language_SingularRuleset_Call) RunAndReturn(run func() pluralizer.Ruleset) *Language_SingularRuleset_Call { + _c.Call.Return(run) + return _c +} + +// NewLanguage creates a new instance of Language. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewLanguage(t interface { + mock.TestingT + Cleanup(func()) +}) *Language { + mock := &Language{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/support/pluralizer/Pattern.go b/mocks/support/pluralizer/Pattern.go new file mode 100644 index 000000000..b55f2df13 --- /dev/null +++ b/mocks/support/pluralizer/Pattern.go @@ -0,0 +1,78 @@ +// Code generated by mockery. DO NOT EDIT. + +package pluralizer + +import mock "github.com/stretchr/testify/mock" + +// Pattern is an autogenerated mock type for the Pattern type +type Pattern struct { + mock.Mock +} + +type Pattern_Expecter struct { + mock *mock.Mock +} + +func (_m *Pattern) EXPECT() *Pattern_Expecter { + return &Pattern_Expecter{mock: &_m.Mock} +} + +// Matches provides a mock function with given fields: word +func (_m *Pattern) Matches(word string) bool { + ret := _m.Called(word) + + if len(ret) == 0 { + panic("no return value specified for Matches") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(word) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Pattern_Matches_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Matches' +type Pattern_Matches_Call struct { + *mock.Call +} + +// Matches is a helper method to define mock.On call +// - word string +func (_e *Pattern_Expecter) Matches(word interface{}) *Pattern_Matches_Call { + return &Pattern_Matches_Call{Call: _e.mock.On("Matches", word)} +} + +func (_c *Pattern_Matches_Call) Run(run func(word string)) *Pattern_Matches_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Pattern_Matches_Call) Return(_a0 bool) *Pattern_Matches_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Pattern_Matches_Call) RunAndReturn(run func(string) bool) *Pattern_Matches_Call { + _c.Call.Return(run) + return _c +} + +// NewPattern creates a new instance of Pattern. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPattern(t interface { + mock.TestingT + Cleanup(func()) +}) *Pattern { + mock := &Pattern{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/support/pluralizer/Ruleset.go b/mocks/support/pluralizer/Ruleset.go new file mode 100644 index 000000000..a0c9e03d0 --- /dev/null +++ b/mocks/support/pluralizer/Ruleset.go @@ -0,0 +1,298 @@ +// Code generated by mockery. DO NOT EDIT. + +package pluralizer + +import ( + pluralizer "github.com/goravel/framework/contracts/support/pluralizer" + mock "github.com/stretchr/testify/mock" +) + +// Ruleset is an autogenerated mock type for the Ruleset type +type Ruleset struct { + mock.Mock +} + +type Ruleset_Expecter struct { + mock *mock.Mock +} + +func (_m *Ruleset) EXPECT() *Ruleset_Expecter { + return &Ruleset_Expecter{mock: &_m.Mock} +} + +// AddIrregular provides a mock function with given fields: substitutions +func (_m *Ruleset) AddIrregular(substitutions ...pluralizer.Substitution) pluralizer.Ruleset { + _va := make([]interface{}, len(substitutions)) + for _i := range substitutions { + _va[_i] = substitutions[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for AddIrregular") + } + + var r0 pluralizer.Ruleset + if rf, ok := ret.Get(0).(func(...pluralizer.Substitution) pluralizer.Ruleset); ok { + r0 = rf(substitutions...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pluralizer.Ruleset) + } + } + + return r0 +} + +// Ruleset_AddIrregular_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddIrregular' +type Ruleset_AddIrregular_Call struct { + *mock.Call +} + +// AddIrregular is a helper method to define mock.On call +// - substitutions ...pluralizer.Substitution +func (_e *Ruleset_Expecter) AddIrregular(substitutions ...interface{}) *Ruleset_AddIrregular_Call { + return &Ruleset_AddIrregular_Call{Call: _e.mock.On("AddIrregular", + append([]interface{}{}, substitutions...)...)} +} + +func (_c *Ruleset_AddIrregular_Call) Run(run func(substitutions ...pluralizer.Substitution)) *Ruleset_AddIrregular_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]pluralizer.Substitution, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(pluralizer.Substitution) + } + } + run(variadicArgs...) + }) + return _c +} + +func (_c *Ruleset_AddIrregular_Call) Return(_a0 pluralizer.Ruleset) *Ruleset_AddIrregular_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Ruleset_AddIrregular_Call) RunAndReturn(run func(...pluralizer.Substitution) pluralizer.Ruleset) *Ruleset_AddIrregular_Call { + _c.Call.Return(run) + return _c +} + +// AddUninflected provides a mock function with given fields: words +func (_m *Ruleset) AddUninflected(words ...string) pluralizer.Ruleset { + _va := make([]interface{}, len(words)) + for _i := range words { + _va[_i] = words[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for AddUninflected") + } + + var r0 pluralizer.Ruleset + if rf, ok := ret.Get(0).(func(...string) pluralizer.Ruleset); ok { + r0 = rf(words...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pluralizer.Ruleset) + } + } + + return r0 +} + +// Ruleset_AddUninflected_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUninflected' +type Ruleset_AddUninflected_Call struct { + *mock.Call +} + +// AddUninflected is a helper method to define mock.On call +// - words ...string +func (_e *Ruleset_Expecter) AddUninflected(words ...interface{}) *Ruleset_AddUninflected_Call { + return &Ruleset_AddUninflected_Call{Call: _e.mock.On("AddUninflected", + append([]interface{}{}, words...)...)} +} + +func (_c *Ruleset_AddUninflected_Call) Run(run func(words ...string)) *Ruleset_AddUninflected_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]string, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + run(variadicArgs...) + }) + return _c +} + +func (_c *Ruleset_AddUninflected_Call) Return(_a0 pluralizer.Ruleset) *Ruleset_AddUninflected_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Ruleset_AddUninflected_Call) RunAndReturn(run func(...string) pluralizer.Ruleset) *Ruleset_AddUninflected_Call { + _c.Call.Return(run) + return _c +} + +// Irregular provides a mock function with no fields +func (_m *Ruleset) Irregular() pluralizer.Substitutions { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Irregular") + } + + var r0 pluralizer.Substitutions + if rf, ok := ret.Get(0).(func() pluralizer.Substitutions); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pluralizer.Substitutions) + } + } + + return r0 +} + +// Ruleset_Irregular_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Irregular' +type Ruleset_Irregular_Call struct { + *mock.Call +} + +// Irregular is a helper method to define mock.On call +func (_e *Ruleset_Expecter) Irregular() *Ruleset_Irregular_Call { + return &Ruleset_Irregular_Call{Call: _e.mock.On("Irregular")} +} + +func (_c *Ruleset_Irregular_Call) Run(run func()) *Ruleset_Irregular_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Ruleset_Irregular_Call) Return(_a0 pluralizer.Substitutions) *Ruleset_Irregular_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Ruleset_Irregular_Call) RunAndReturn(run func() pluralizer.Substitutions) *Ruleset_Irregular_Call { + _c.Call.Return(run) + return _c +} + +// Regular provides a mock function with no fields +func (_m *Ruleset) Regular() pluralizer.Transformations { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Regular") + } + + var r0 pluralizer.Transformations + if rf, ok := ret.Get(0).(func() pluralizer.Transformations); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pluralizer.Transformations) + } + } + + return r0 +} + +// Ruleset_Regular_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Regular' +type Ruleset_Regular_Call struct { + *mock.Call +} + +// Regular is a helper method to define mock.On call +func (_e *Ruleset_Expecter) Regular() *Ruleset_Regular_Call { + return &Ruleset_Regular_Call{Call: _e.mock.On("Regular")} +} + +func (_c *Ruleset_Regular_Call) Run(run func()) *Ruleset_Regular_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Ruleset_Regular_Call) Return(_a0 pluralizer.Transformations) *Ruleset_Regular_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Ruleset_Regular_Call) RunAndReturn(run func() pluralizer.Transformations) *Ruleset_Regular_Call { + _c.Call.Return(run) + return _c +} + +// Uninflected provides a mock function with no fields +func (_m *Ruleset) Uninflected() pluralizer.Patterns { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Uninflected") + } + + var r0 pluralizer.Patterns + if rf, ok := ret.Get(0).(func() pluralizer.Patterns); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pluralizer.Patterns) + } + } + + return r0 +} + +// Ruleset_Uninflected_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Uninflected' +type Ruleset_Uninflected_Call struct { + *mock.Call +} + +// Uninflected is a helper method to define mock.On call +func (_e *Ruleset_Expecter) Uninflected() *Ruleset_Uninflected_Call { + return &Ruleset_Uninflected_Call{Call: _e.mock.On("Uninflected")} +} + +func (_c *Ruleset_Uninflected_Call) Run(run func()) *Ruleset_Uninflected_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Ruleset_Uninflected_Call) Return(_a0 pluralizer.Patterns) *Ruleset_Uninflected_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Ruleset_Uninflected_Call) RunAndReturn(run func() pluralizer.Patterns) *Ruleset_Uninflected_Call { + _c.Call.Return(run) + return _c +} + +// NewRuleset creates a new instance of Ruleset. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRuleset(t interface { + mock.TestingT + Cleanup(func()) +}) *Ruleset { + mock := &Ruleset{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/support/pluralizer/Substitution.go b/mocks/support/pluralizer/Substitution.go new file mode 100644 index 000000000..1c55edde2 --- /dev/null +++ b/mocks/support/pluralizer/Substitution.go @@ -0,0 +1,122 @@ +// Code generated by mockery. DO NOT EDIT. + +package pluralizer + +import mock "github.com/stretchr/testify/mock" + +// Substitution is an autogenerated mock type for the Substitution type +type Substitution struct { + mock.Mock +} + +type Substitution_Expecter struct { + mock *mock.Mock +} + +func (_m *Substitution) EXPECT() *Substitution_Expecter { + return &Substitution_Expecter{mock: &_m.Mock} +} + +// From provides a mock function with no fields +func (_m *Substitution) From() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for From") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Substitution_From_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'From' +type Substitution_From_Call struct { + *mock.Call +} + +// From is a helper method to define mock.On call +func (_e *Substitution_Expecter) From() *Substitution_From_Call { + return &Substitution_From_Call{Call: _e.mock.On("From")} +} + +func (_c *Substitution_From_Call) Run(run func()) *Substitution_From_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Substitution_From_Call) Return(_a0 string) *Substitution_From_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Substitution_From_Call) RunAndReturn(run func() string) *Substitution_From_Call { + _c.Call.Return(run) + return _c +} + +// To provides a mock function with no fields +func (_m *Substitution) To() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for To") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Substitution_To_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'To' +type Substitution_To_Call struct { + *mock.Call +} + +// To is a helper method to define mock.On call +func (_e *Substitution_Expecter) To() *Substitution_To_Call { + return &Substitution_To_Call{Call: _e.mock.On("To")} +} + +func (_c *Substitution_To_Call) Run(run func()) *Substitution_To_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Substitution_To_Call) Return(_a0 string) *Substitution_To_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Substitution_To_Call) RunAndReturn(run func() string) *Substitution_To_Call { + _c.Call.Return(run) + return _c +} + +// NewSubstitution creates a new instance of Substitution. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSubstitution(t interface { + mock.TestingT + Cleanup(func()) +}) *Substitution { + mock := &Substitution{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/support/pluralizer/Transformation.go b/mocks/support/pluralizer/Transformation.go new file mode 100644 index 000000000..3acb06f75 --- /dev/null +++ b/mocks/support/pluralizer/Transformation.go @@ -0,0 +1,78 @@ +// Code generated by mockery. DO NOT EDIT. + +package pluralizer + +import mock "github.com/stretchr/testify/mock" + +// Transformation is an autogenerated mock type for the Transformation type +type Transformation struct { + mock.Mock +} + +type Transformation_Expecter struct { + mock *mock.Mock +} + +func (_m *Transformation) EXPECT() *Transformation_Expecter { + return &Transformation_Expecter{mock: &_m.Mock} +} + +// Apply provides a mock function with given fields: word +func (_m *Transformation) Apply(word string) string { + ret := _m.Called(word) + + if len(ret) == 0 { + panic("no return value specified for Apply") + } + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(word) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Transformation_Apply_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Apply' +type Transformation_Apply_Call struct { + *mock.Call +} + +// Apply is a helper method to define mock.On call +// - word string +func (_e *Transformation_Expecter) Apply(word interface{}) *Transformation_Apply_Call { + return &Transformation_Apply_Call{Call: _e.mock.On("Apply", word)} +} + +func (_c *Transformation_Apply_Call) Run(run func(word string)) *Transformation_Apply_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Transformation_Apply_Call) Return(_a0 string) *Transformation_Apply_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Transformation_Apply_Call) RunAndReturn(run func(string) string) *Transformation_Apply_Call { + _c.Call.Return(run) + return _c +} + +// NewTransformation creates a new instance of Transformation. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewTransformation(t interface { + mock.TestingT + Cleanup(func()) +}) *Transformation { + mock := &Transformation{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/support/pluralizer/english/english.go b/support/pluralizer/english/english.go new file mode 100644 index 000000000..20c82ec43 --- /dev/null +++ b/support/pluralizer/english/english.go @@ -0,0 +1,341 @@ +// Package english provides a comprehensive set of rules for English pluralization +// and singularization. It's the result of a deep dive into English morphology, +// designed to be highly accurate by respecting the complex hierarchy of its rules. +// +// The core philosophy is that inflection isn't a single step, but a cascade: a word +// is first checked against a list of truly invariable words (uninflected), then +// against a list of unique irregular words, and only then is it processed by the +// general pattern-based rules. +// +// A key principle in this library is the careful distinction between nouns that are +// sometimes uncountable (like "work") and those that are almost always invariable +// (like "information"). To allow for correct pluralization of words like "work" +// into "works" or "permission" into "permissions", the uninflected list is +// intentionally kept strict and concise. +// +// Sources and references used for curation: +// +// - English Plurals (Wikipedia): For the foundational rules of regular, +// irregular, and loanword pluralization. +// https://en.wikipedia.org/wiki/English_plurals +// +// - Mass Nouns / Uncountable Nouns (Wikipedia): To understand the principles +// behind uncountable nouns and curate the uninflected list. +// https://en.wikipedia.org/wiki/Mass_noun +// +// - Plurale & Singulare Tantum (Wikipedia): For handling plural-only and +// singular-only nouns like "scissors" and "news". +// https://en.wikipedia.org/wiki/Plurale_tantum +// https://en.wikipedia.org/wiki/Singulare_tantum +// +// - Grammarist - Uncountable Nouns: Provided a practical list that helped +// refine the distinction between contextually and strictly uncountable nouns. +// https://grammarist.com/grammar/uncountable-nouns/ +// +// - Wiktionary, the free dictionary: Used extensively for cross-referencing +// the plural forms and usage notes of individual, ambiguous words. +// https://en.wiktionary.org/ +// +// - Doctrine Inflector (GitHub): For analysis of a robust, widely-used, and +// battle-tested inflection library. +// https://github.com/doctrine/inflector +package english + +import ( + "github.com/goravel/framework/contracts/support/pluralizer" + "github.com/goravel/framework/support/pluralizer/rules" +) + +var _ pluralizer.Language = (*Language)(nil) + +type Language struct { + singularRuleset pluralizer.Ruleset + pluralRuleset pluralizer.Ruleset +} + +func New() *Language { + return &Language{ + singularRuleset: newEnglishSingularRuleset(), + pluralRuleset: newEnglishPluralRuleset(), + } +} + +func (r *Language) Name() string { + return "english" +} + +func (r *Language) SingularRuleset() pluralizer.Ruleset { + return r.singularRuleset +} + +func (r *Language) PluralRuleset() pluralizer.Ruleset { + return r.pluralRuleset +} + +func newEnglishPluralRuleset() pluralizer.Ruleset { + uninflected := getUninflectedDefault() + uninflected = append(uninflected, + rules.NewPattern(`(?i)media$`), + rules.NewPattern(`(?i)people$`), + rules.NewPattern(`(?i)trivia$`), + rules.NewPattern(`(?i)\w+ware$`), + ) + + irregular := pluralizer.Substitutions{ + rules.NewSubstitution("atlas", "atlases"), + rules.NewSubstitution("brother", "brothers"), + rules.NewSubstitution("brother-in-law", "brothers-in-law"), + rules.NewSubstitution("cafe", "cafes"), + rules.NewSubstitution("chateau", "chateaux"), + rules.NewSubstitution("child", "children"), + rules.NewSubstitution("cookie", "cookies"), + rules.NewSubstitution("corpus", "corpora"), + rules.NewSubstitution("criterion", "criteria"), + rules.NewSubstitution("curriculum", "curricula"), + rules.NewSubstitution("daughter-in-law", "daughters-in-law"), + rules.NewSubstitution("demo", "demos"), + rules.NewSubstitution("die", "dice"), + rules.NewSubstitution("domino", "dominoes"), + rules.NewSubstitution("echo", "echoes"), + rules.NewSubstitution("father-in-law", "fathers-in-law"), + rules.NewSubstitution("foe", "foes"), + rules.NewSubstitution("foot", "feet"), + rules.NewSubstitution("fungus", "fungi"), + rules.NewSubstitution("genie", "genies"), + rules.NewSubstitution("genus", "genera"), + rules.NewSubstitution("goose", "geese"), + rules.NewSubstitution("graffito", "graffiti"), + rules.NewSubstitution("hippopotamus", "hippopotami"), + rules.NewSubstitution("iris", "irises"), + rules.NewSubstitution("larva", "larvae"), + rules.NewSubstitution("leaf", "leaves"), + rules.NewSubstitution("lens", "lenses"), + rules.NewSubstitution("loaf", "loaves"), + rules.NewSubstitution("louse", "lice"), + rules.NewSubstitution("man", "men"), + rules.NewSubstitution("memorandum", "memoranda"), + rules.NewSubstitution("mongoose", "mongooses"), + rules.NewSubstitution("mother-in-law", "mothers-in-law"), + rules.NewSubstitution("motto", "mottoes"), + rules.NewSubstitution("mouse", "mice"), + rules.NewSubstitution("move", "moves"), + rules.NewSubstitution("mythos", "mythoi"), + rules.NewSubstitution("nucleus", "nuclei"), + rules.NewSubstitution("oasis", "oases"), + rules.NewSubstitution("octopus", "octopuses"), + rules.NewSubstitution("opus", "opuses"), + rules.NewSubstitution("ox", "oxen"), + rules.NewSubstitution("passer-by", "passers-by"), + rules.NewSubstitution("passerby", "passersby"), + rules.NewSubstitution("phenomenon", "phenomena"), + rules.NewSubstitution("person", "people"), + rules.NewSubstitution("plateau", "plateaux"), + rules.NewSubstitution("runner-up", "runners-up"), + rules.NewSubstitution("sex", "sexes"), + rules.NewSubstitution("sister-in-law", "sisters-in-law"), + rules.NewSubstitution("soliloquy", "soliloquies"), + rules.NewSubstitution("son-in-law", "sons-in-law"), + rules.NewSubstitution("syllabus", "syllabi"), + rules.NewSubstitution("testis", "testes"), + rules.NewSubstitution("thief", "thieves"), + rules.NewSubstitution("tooth", "teeth"), + rules.NewSubstitution("tornado", "tornadoes"), + rules.NewSubstitution("volcano", "volcanoes"), + rules.NewSubstitution("woman", "women"), + rules.NewSubstitution("zombie", "zombies"), + } + + regular := pluralizer.Transformations{ + rules.NewTransformation(`(?i)(quiz)$`, `${1}zes`), + rules.NewTransformation(`(?i)(matr|vert|ind)(ix|ex)$`, `${1}ices`), + rules.NewTransformation(`(?i)(x|ch|ss|sh|z)$`, `${1}es`), + rules.NewTransformation(`(?i)([^aeiouy]|qu)y$`, `${1}ies`), + rules.NewTransformation(`(?i)(hive|gulf)$`, `${1}s`), + rules.NewTransformation(`(?i)(?:([^f])fe|([lr])f)$`, `${1}${2}ves`), + rules.NewTransformation(`(?i)sis$`, `ses`), + rules.NewTransformation(`(?i)([ti])um$`, `${1}a`), + rules.NewTransformation(`(?i)(tax)on$`, `${1}a`), + rules.NewTransformation(`(?i)(buffal|her|potat|tomat|volcan)o$`, `${1}oes`), + rules.NewTransformation(`(?i)(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$`, `${1}i`), + rules.NewTransformation(`(?i)us$`, `uses`), + rules.NewTransformation(`(?i)(alias|status)$`, `${1}es`), + rules.NewTransformation(`(?i)(analys|ax|cris|test|thes)is$`, `${1}es`), + rules.NewTransformation(`(?i)s$`, `s`), + rules.NewTransformation(`(?i)$`, `s`), + } + + return rules.NewRuleset(regular, uninflected, irregular) +} + +func newEnglishSingularRuleset() pluralizer.Ruleset { + uninflected := getUninflectedDefault() + uninflected = append(uninflected, + rules.NewPattern(`(?i).*ss$`), + rules.NewPattern(`(?i)athletics$`), + rules.NewPattern(`(?i)data$`), + rules.NewPattern(`(?i)electronics$`), + rules.NewPattern(`(?i)genetics$`), + rules.NewPattern(`(?i)graphics$`), + rules.NewPattern(`(?i)jeans$`), + rules.NewPattern(`(?i)mathematics$`), + rules.NewPattern(`(?i)news$`), + rules.NewPattern(`(?i)pliers$`), + rules.NewPattern(`(?i)politics$`), + rules.NewPattern(`(?i)scissors$`), + rules.NewPattern(`(?i)shorts$`), + rules.NewPattern(`(?i)trousers$`), + rules.NewPattern(`(?i)trivia$`), + ) + + irregular := pluralizer.Substitutions{} + for _, sub := range newEnglishPluralRuleset().Irregular() { + irregular = append(irregular, rules.NewSubstitution(sub.To(), sub.From())) + } + + regular := pluralizer.Transformations{ + rules.NewTransformation(`(?i)(s)tatuses$`, `${1}tatus`), + rules.NewTransformation(`(?i)(quiz)zes$`, `${1}`), + rules.NewTransformation(`(?i)(matr)ices$`, `${1}ix`), + rules.NewTransformation(`(?i)(vert|ind)ices$`, `${1}ex`), + rules.NewTransformation(`(?i)^(ox)en`, `${1}`), + rules.NewTransformation(`(?i)(alias|status)(es)?$`, `${1}`), + rules.NewTransformation(`(?i)(buffal|her|potat|tomat|volcan)oes$`, `${1}o`), + rules.NewTransformation(`(?i)(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$`, `${1}us`), + rules.NewTransformation(`(?i)([ftw]ax)es$`, `${1}`), + rules.NewTransformation(`(?i)(analys|ax|cris|test|thes)es$`, `${1}is`), + rules.NewTransformation(`(?i)(shoe|slave)s$`, `${1}`), + rules.NewTransformation(`(?i)(o)es$`, `${1}`), + rules.NewTransformation(`(?i)ouses$`, `ouse`), + rules.NewTransformation(`(?i)([^a])uses$`, `${1}us`), + rules.NewTransformation(`(?i)([ml])ice$`, `${1}ouse`), + rules.NewTransformation(`(?i)(x|ch|ss|sh|z)es$`, `${1}`), + rules.NewTransformation(`(?i)(m)ovies$`, `${1}ovie`), + rules.NewTransformation(`(?i)(s)eries$`, `${1}eries`), + rules.NewTransformation(`(?i)([^aeiouy]|qu)ies$`, `${1}y`), + rules.NewTransformation(`(?i)([lr])ves$`, `${1}f`), + rules.NewTransformation(`(?i)(tive)s$`, `${1}`), + rules.NewTransformation(`(?i)(hive)s$`, `${1}`), + rules.NewTransformation(`(?i)([^fo])ves$`, `${1}fe`), + rules.NewTransformation(`(?i)(^analy)ses$`, `${1}sis`), + rules.NewTransformation(`(?i)(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$`, `${1}${2}sis`), + rules.NewTransformation(`(?i)(tax)a$`, `${1}on`), + rules.NewTransformation(`(?i)(c)riteria$`, `${1}riterion`), + rules.NewTransformation(`(?i)([ti])a$`, `${1}um`), + rules.NewTransformation(`(?i)eaus$`, `eau`), + rules.NewTransformation(`(?i)s$`, ``), + } + + return rules.NewRuleset(regular, uninflected, irregular) +} + +// getUninflectedDefault returns a list of nouns that are truly uninflected. +// This list is carefully curated to contain only pure mass nouns and words with +// identical singular/plural forms, preventing incorrect behavior for words that +// can be both countable and uncountable (e.g., "permission" -> "permissions"). +func getUninflectedDefault() []pluralizer.Pattern { + return []pluralizer.Pattern{ + // Truly uncountable nouns + rules.NewPattern(`(?i)advice$`), + rules.NewPattern(`(?i)baggage$`), + rules.NewPattern(`(?i)butter$`), + rules.NewPattern(`(?i)clothing$`), + rules.NewPattern(`(?i)coal$`), + rules.NewPattern(`(?i)debris$`), + rules.NewPattern(`(?i)education$`), + rules.NewPattern(`(?i)equipment$`), + rules.NewPattern(`(?i)evidence$`), + rules.NewPattern(`(?i)feedback$`), + rules.NewPattern(`(?i)food$`), + rules.NewPattern(`(?i)furniture$`), + rules.NewPattern(`(?i)homework$`), + rules.NewPattern(`(?i)information$`), + rules.NewPattern(`(?i)knowledge$`), + rules.NewPattern(`(?i)leather$`), + rules.NewPattern(`(?i)luggage$`), + rules.NewPattern(`(?i)money$`), + rules.NewPattern(`(?i)music$`), + rules.NewPattern(`(?i)plankton$`), + rules.NewPattern(`(?i)progress$`), + rules.NewPattern(`(?i)rain$`), + rules.NewPattern(`(?i)research$`), + rules.NewPattern(`(?i)rice$`), + rules.NewPattern(`(?i)sand$`), + rules.NewPattern(`(?i)spam$`), + rules.NewPattern(`(?i)sugar$`), + rules.NewPattern(`(?i)traffic$`), + rules.NewPattern(`(?i)water$`), + rules.NewPattern(`(?i)weather$`), + rules.NewPattern(`(?i)wheat$`), + rules.NewPattern(`(?i)wood$`), + rules.NewPattern(`(?i)wool$`), + + // Nouns with identical singular and plural forms (zero-plurals) + rules.NewPattern(`(?i)aircraft$`), + rules.NewPattern(`(?i)bison$`), + rules.NewPattern(`(?i)buffalo$`), + rules.NewPattern(`(?i)chassis$`), + rules.NewPattern(`(?i)cod$`), + rules.NewPattern(`(?i)corps$`), + rules.NewPattern(`(?i)deer$`), + rules.NewPattern(`(?i)fish$`), + rules.NewPattern(`(?i)flounder$`), + rules.NewPattern(`(?i)jedi$`), + rules.NewPattern(`(?i)mackerel$`), + rules.NewPattern(`(?i)moose$`), + rules.NewPattern(`(?i)offspring$`), + rules.NewPattern(`(?i)pokemon$`), + rules.NewPattern(`(?i)salmon$`), + rules.NewPattern(`(?i)series$`), + rules.NewPattern(`(?i)sheep$`), + rules.NewPattern(`(?i)shrimp$`), + rules.NewPattern(`(?i)species$`), + rules.NewPattern(`(?i)swine$`), + rules.NewPattern(`(?i)trout$`), + rules.NewPattern(`(?i)tuna$`), + + // Plural-only nouns (pluralia tantum) + rules.NewPattern(`(?i)belongings$`), + rules.NewPattern(`(?i)binoculars$`), + rules.NewPattern(`(?i)cattle$`), + rules.NewPattern(`(?i)clothes$`), + rules.NewPattern(`(?i)congratulations$`), + rules.NewPattern(`(?i)jeans$`), + rules.NewPattern(`(?i)pants$`), + rules.NewPattern(`(?i)pliers$`), + rules.NewPattern(`(?i)police$`), + rules.NewPattern(`(?i)scissors$`), + rules.NewPattern(`(?i)shorts$`), + rules.NewPattern(`(?i)thanks$`), + rules.NewPattern(`(?i)trousers$`), + + // Singular-only nouns that end in -s (singularia tantum) + rules.NewPattern(`(?i)athletics$`), + rules.NewPattern(`(?i)billiards$`), + rules.NewPattern(`(?i)diabetes$`), + rules.NewPattern(`(?i)economics$`), + rules.NewPattern(`(?i)ethics$`), + rules.NewPattern(`(?i)gallows$`), + rules.NewPattern(`(?i)gymnastics$`), + rules.NewPattern(`(?i)innings$`), + rules.NewPattern(`(?i)linguistics$`), + rules.NewPattern(`(?i)mathematics$`), + rules.NewPattern(`(?i)measles$`), + rules.NewPattern(`(?i)mumps$`), + rules.NewPattern(`(?i)news$`), + rules.NewPattern(`(?i)nexus$`), + rules.NewPattern(`(?i)physics$`), + rules.NewPattern(`(?i)politics$`), + rules.NewPattern(`(?i)rabies$`), + rules.NewPattern(`(?i)rickets$`), + rules.NewPattern(`(?i)shingles$`), + rules.NewPattern(`(?i)statistics$`), + + // Other special cases + rules.NewPattern(`(?i)audio$`), + rules.NewPattern(`(?i)data$`), + rules.NewPattern(`(?i)emoji$`), + rules.NewPattern(`(?i)metadata$`), + rules.NewPattern(`(?i)sms$`), + rules.NewPattern(`(?i)staff$`), + } +} diff --git a/support/pluralizer/english/english_test.go b/support/pluralizer/english/english_test.go new file mode 100644 index 000000000..0d0cabcfa --- /dev/null +++ b/support/pluralizer/english/english_test.go @@ -0,0 +1,179 @@ +package english + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/goravel/framework/support/pluralizer/inflector" +) + +func TestEnglishIrregularPlurals(t *testing.T) { + lang := New() + infl := inflector.New(lang) + + irregulars := map[string]string{ + "person": "people", + "child": "children", + "foot": "feet", + "tooth": "teeth", + "man": "men", + "woman": "women", + "goose": "geese", + "mouse": "mice", + "ox": "oxen", + "axis": "axes", + "crisis": "crises", + "thesis": "theses", + "phenomenon": "phenomena", + "criterion": "criteria", + "medium": "media", + "alumnus": "alumni", + "cactus": "cacti", + "fungus": "fungi", + "corpus": "corpora", + "genus": "genera", + "stimulus": "stimuli", + "syllabus": "syllabi", + "synopsis": "synopses", + "atlas": "atlases", + "lens": "lenses", + "octopus": "octopuses", + "plateau": "plateaux", + "chateau": "chateaux", + } + + for singular, plural := range irregulars { + assert.Equal(t, plural, infl.Plural(singular)) + assert.Equal(t, singular, infl.Singular(plural)) + } +} + +func TestEnglishRegularPatterns(t *testing.T) { + lang := New() + infl := inflector.New(lang) + + tests := []struct { + rule string + singular string + plural string + }{ + {"words ending in s/x/z/ch/sh", "bus", "buses"}, + {"words ending in s/x/z/ch/sh", "box", "boxes"}, + {"words ending in s/x/z/ch/sh", "buzz", "buzzes"}, + {"words ending in s/x/z/ch/sh", "church", "churches"}, + {"words ending in s/x/z/ch/sh", "dish", "dishes"}, + + {"words ending in consonant+y", "city", "cities"}, + {"words ending in consonant+y", "baby", "babies"}, + {"words ending in consonant+y", "party", "parties"}, + {"words ending in consonant+y", "company", "companies"}, + {"words ending in vowel+y", "toy", "toys"}, + {"words ending in vowel+y", "boy", "boys"}, + + {"words ending in f/fe", "life", "lives"}, + {"words ending in f/fe", "wife", "wives"}, + {"words ending in f/fe", "wolf", "wolves"}, + {"words ending in f/fe", "shelf", "shelves"}, + {"words ending in f/fe", "leaf", "leaves"}, + + {"words ending in o", "hero", "heroes"}, + {"words ending in o", "potato", "potatoes"}, + {"words ending in o", "tomato", "tomatoes"}, + + {"regular words", "book", "books"}, + {"regular words", "table", "tables"}, + {"regular words", "car", "cars"}, + } + + for _, test := range tests { + assert.Equal(t, test.plural, infl.Plural(test.singular)) + assert.Equal(t, test.singular, infl.Singular(test.plural)) + } +} + +func TestEnglishUncountableWords(t *testing.T) { + lang := New() + infl := inflector.New(lang) + + uncountable := []string{ + "sheep", "deer", "fish", "moose", "swine", + "bison", "salmon", "trout", "species", + "information", "equipment", "money", "advice", + "news", "data", + "furniture", "luggage", "baggage", "butter", + "research", "traffic", "weather", + } + + for _, word := range uncountable { + assert.Equal(t, word, infl.Plural(word)) + assert.Equal(t, word, infl.Singular(word)) + } +} + +func TestEnglishCompoundWords(t *testing.T) { + lang := New() + infl := inflector.New(lang) + + compounds := map[string]string{ + "son-in-law": "sons-in-law", + "daughter-in-law": "daughters-in-law", + "runner-up": "runners-up", + "passer-by": "passers-by", + "mother-in-law": "mothers-in-law", + } + + for singular, plural := range compounds { + assert.Equal(t, plural, infl.Plural(singular)) + assert.Equal(t, singular, infl.Singular(plural)) + } +} + +func TestEnglishSpecialCases(t *testing.T) { + lang := New() + infl := inflector.New(lang) + + tests := []struct { + singular string + plural string + }{ + {"quiz", "quizzes"}, + {"analysis", "analyses"}, + {"basis", "bases"}, + {"diagnosis", "diagnoses"}, + {"hypothesis", "hypotheses"}, + {"oasis", "oases"}, + {"parenthesis", "parentheses"}, + {"synopsis", "synopses"}, + {"bacterium", "bacteria"}, + {"curriculum", "curricula"}, + {"erratum", "errata"}, + {"memorandum", "memoranda"}, + {"millennium", "millennia"}, + {"stratum", "strata"}, + {"symposium", "symposia"}, + } + + for _, test := range tests { + assert.Equal(t, test.plural, infl.Plural(test.singular)) + assert.Equal(t, test.singular, infl.Singular(test.plural)) + } +} + +func TestEnglishRulesetStructure(t *testing.T) { + lang := New() + + assert.Equal(t, "english", lang.Name()) + assert.NotNil(t, lang.PluralRuleset()) + assert.NotNil(t, lang.SingularRuleset()) + + pluralRules := lang.PluralRuleset() + singularRules := lang.SingularRuleset() + + assert.True(t, len(pluralRules.Irregular()) > 0) + assert.True(t, len(singularRules.Irregular()) > 0) + assert.True(t, len(pluralRules.Regular()) > 0) + assert.True(t, len(singularRules.Regular()) > 0) + assert.True(t, len(pluralRules.Uninflected()) > 0) + assert.True(t, len(singularRules.Uninflected()) > 0) +} diff --git a/support/pluralizer/inflector/inflector.go b/support/pluralizer/inflector/inflector.go new file mode 100644 index 000000000..659dda9c1 --- /dev/null +++ b/support/pluralizer/inflector/inflector.go @@ -0,0 +1,69 @@ +package inflector + +import ( + "strings" + + "github.com/goravel/framework/contracts/support/pluralizer" +) + +type Inflector struct { + language pluralizer.Language +} + +func New(language pluralizer.Language) pluralizer.Inflector { + return &Inflector{ + language: language, + } +} + +func (r *Inflector) Language() pluralizer.Language { + return r.language +} + +func (r *Inflector) Plural(word string) string { + return r.inflect(word, r.language.PluralRuleset()) +} + +func (r *Inflector) SetLanguage(language pluralizer.Language) pluralizer.Inflector { + r.language = language + + return r +} + +func (r *Inflector) Singular(word string) string { + return r.inflect(word, r.language.SingularRuleset()) +} + +func (r *Inflector) inflect(word string, ruleset pluralizer.Ruleset) string { + if word == "" { + return "" + } + + for _, pattern := range ruleset.Uninflected() { + if pattern.Matches(word) { + return word + } + } + + // Check if word is already in target form (To) + for _, substitution := range ruleset.Irregular() { + if strings.EqualFold(word, substitution.To()) { + return word + } + } + + // Check if word is in source form (From) and convert to target form (To) + for _, substitution := range ruleset.Irregular() { + if strings.EqualFold(word, substitution.From()) { + return MatchCase(substitution.To(), word) + } + } + + for _, transformation := range ruleset.Regular() { + if result := transformation.Apply(word); result != "" { + return MatchCase(result, word) + } + } + + return word +} diff --git a/support/pluralizer/inflector/inflector_test.go b/support/pluralizer/inflector/inflector_test.go new file mode 100644 index 000000000..4f48729fe --- /dev/null +++ b/support/pluralizer/inflector/inflector_test.go @@ -0,0 +1,123 @@ +package inflector + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/goravel/framework/support/pluralizer/english" +) + +func TestInflectorPlural(t *testing.T) { + inflector := New(english.New()) + + tests := []struct { + input string + expected string + }{ + {"", ""}, + {"book", "books"}, + {"person", "people"}, + {"child", "children"}, + {"mouse", "mice"}, + {"sheep", "sheep"}, + {"data", "data"}, + {"city", "cities"}, + {"half", "halves"}, + {"quiz", "quizzes"}, + {"ox", "oxen"}, + } + + for _, test := range tests { + assert.Equal(t, test.expected, inflector.Plural(test.input)) + } +} + +func TestInflectorSingular(t *testing.T) { + inflector := New(english.New()) + + tests := []struct { + input string + expected string + }{ + {"", ""}, + {"books", "book"}, + {"people", "person"}, + {"children", "child"}, + {"mice", "mouse"}, + {"sheep", "sheep"}, + {"data", "data"}, + {"cities", "city"}, + {"halves", "half"}, + {"quizzes", "quiz"}, + {"oxen", "ox"}, + } + + for _, test := range tests { + assert.Equal(t, test.expected, inflector.Singular(test.input)) + } +} + +func TestInflectorCasePreservation(t *testing.T) { + inflector := New(english.New()) + + tests := []struct { + input string + expected string + method string + }{ + {"BOOK", "BOOKS", "plural"}, + {"Book", "Books", "plural"}, + {"book", "books", "plural"}, + {"BOOKS", "BOOK", "singular"}, + {"Books", "Book", "singular"}, + {"PERSON", "PEOPLE", "plural"}, + {"Person", "People", "plural"}, + {"PEOPLE", "PERSON", "singular"}, + {"People", "Person", "singular"}, + } + + for _, test := range tests { + if test.method == "plural" { + assert.Equal(t, test.expected, inflector.Plural(test.input)) + } else { + assert.Equal(t, test.expected, inflector.Singular(test.input)) + } + } +} + +func TestInflectorUncountableWords(t *testing.T) { + inflector := New(english.New()) + + uncountable := []string{ + "fish", "sheep", "deer", "moose", "swine", + "information", "equipment", "money", "advice", + "software", "news", "data", + } + + for _, word := range uncountable { + assert.Equal(t, word, inflector.Plural(word)) + assert.Equal(t, word, inflector.Singular(word)) + } +} + +func TestInflectorIrregularWords(t *testing.T) { + inflector := New(english.New()) + + irregulars := map[string]string{ + "person": "people", + "child": "children", + "foot": "feet", + "tooth": "teeth", + "goose": "geese", + "man": "men", + "woman": "women", + "mouse": "mice", + "ox": "oxen", + } + + for singular, plural := range irregulars { + assert.Equal(t, plural, inflector.Plural(singular)) + assert.Equal(t, singular, inflector.Singular(plural)) + } +} diff --git a/support/pluralizer/inflector/match_case.go b/support/pluralizer/inflector/match_case.go new file mode 100644 index 000000000..70422cc29 --- /dev/null +++ b/support/pluralizer/inflector/match_case.go @@ -0,0 +1,123 @@ +package inflector + +import ( + "strings" + "unicode" +) + +const ( + styleFallback = iota + styleAllLower + styleAllUpper + styleUcFirst + styleUcWords +) + +// MatchCase transforms s to match the casing style of pattern. +func MatchCase(s, pattern string) string { + switch detectPatternStyle(pattern) { + case styleAllLower: + return strings.ToLower(s) + case styleAllUpper: + return strings.ToUpper(s) + case styleUcFirst: + return UcFirst(s) + case styleUcWords: + return UcWords(s) + default: + return s + } +} + +// detectPatternStyle scans pattern once to determine its case style. +func detectPatternStyle(p string) int { + hasLetter := false + isAllLower, isAllUpper := true, true + firstLetter, firstLetterSeen := rune(0), false + isUcFirst, isUcWords := true, true + inWord := false + + for _, r := range p { + if !unicode.IsLetter(r) { + inWord = false + continue + } + hasLetter = true + + if !firstLetterSeen { + firstLetter = r + firstLetterSeen = true + } else if !unicode.IsLower(r) { + isUcFirst = false + } + + if !unicode.IsLower(r) { + isAllLower = false + } + if !unicode.IsUpper(r) { + isAllUpper = false + } + + if inWord { + if !unicode.IsLower(r) { + isUcWords = false + } + } else { + if !unicode.IsUpper(r) { + isUcWords = false + } + inWord = true + } + } + + switch { + case hasLetter && isAllLower: + return styleAllLower + case hasLetter && isAllUpper: + return styleAllUpper + case hasLetter && unicode.IsUpper(firstLetter) && isUcFirst: + return styleUcFirst + case hasLetter && isUcWords: + return styleUcWords + default: + return styleFallback + } +} + +// UcFirst uppercases the first letter and lowercases the rest of the first word. +func UcFirst(s string) string { + runes := []rune(s) + start := -1 + for i, r := range runes { + if unicode.IsLetter(r) { + if start == -1 { + runes[i] = unicode.ToUpper(r) + start = i + } else { + runes[i] = unicode.ToLower(r) + } + } else if start != -1 { + break + } + } + return string(runes) +} + +// UcWords uppercases the first letter of each word, lowercases the rest. +func UcWords(s string) string { + runes := []rune(s) + inWord := false + for i, r := range runes { + if unicode.IsLetter(r) { + if !inWord { + runes[i] = unicode.ToUpper(r) + inWord = true + } else { + runes[i] = unicode.ToLower(r) + } + } else { + inWord = false + } + } + return string(runes) +} diff --git a/support/pluralizer/inflector/match_case_test.go b/support/pluralizer/inflector/match_case_test.go new file mode 100644 index 000000000..a2a8e3805 --- /dev/null +++ b/support/pluralizer/inflector/match_case_test.go @@ -0,0 +1,115 @@ +package inflector + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMatchCase(t *testing.T) { + tests := []struct { + name string + word string + comparison string + expected string + description string + }{ + {"empty word", "", "ANYTHING", "", "empty word should remain empty"}, + {"empty comparison", "word", "", "word", "empty comparison should not change word"}, + {"lowercase to uppercase", "word", "COMP", "WORD", "should convert to uppercase"}, + {"uppercase to lowercase", "WORD", "comp", "word", "should convert to lowercase"}, + {"mixed to uppercase", "WoRd", "COMP", "WORD", "should convert to uppercase"}, + {"mixed to lowercase", "WoRd", "comp", "word", "should convert to lowercase"}, + {"lowercase to title case", "word", "Comp", "Word", "should convert to title case"}, + {"uppercase to title case", "WORD", "Comp", "Word", "should convert to title case"}, + {"title case to lowercase", "Word", "comp", "word", "should convert to lowercase"}, + {"title case to uppercase", "Word", "COMP", "WORD", "should convert to uppercase"}, + {"title case to title case", "Word", "Comp", "Word", "should remain title case"}, + {"longer word to title case", "complicated", "Title", "Complicated", "should convert longer word to title case"}, + {"with numbers and symbols", "word123!@#", "COMP", "WORD123!@#", "should preserve numbers and symbols"}, + {"with leading numbers", "123word", "COMP", "123WORD", "should preserve leading numbers"}, + {"with leading symbols", "!@#word", "Comp", "!@#Word", "should preserve leading symbols"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := MatchCase(test.word, test.comparison) + assert.Equal(t, test.expected, result, "Input: %s, Comparison: %s - %s", test.word, test.comparison, test.description) + }) + } +} + +func TestMatchCaseWithSpecialPatterns(t *testing.T) { + tests := []struct { + name string + word string + comparison string + expected string + description string + }{ + {"unicode characters", "café", "UPPER", "CAFÉ", "should handle unicode characters"}, + {"mixed unicode", "café", "Title", "Café", "should handle mixed unicode in title case"}, + {"empty word with title case", "", "Title", "", "empty word should remain empty regardless of pattern"}, + {"single character to title", "x", "Title", "X", "single character should be capitalized for title case"}, + {"single character to upper", "x", "UPPER", "X", "single character should be capitalized for upper case"}, + {"single character to lower", "X", "lower", "x", "single character should be lowercase for lower case"}, + {"non-letter characters only", "123!@#", "UPPER", "123!@#", "non-letter characters should remain unchanged"}, + {"non-letter characters only to title", "123!@#", "Title", "123!@#", "non-letter characters should remain unchanged for title case"}, + {"ucwords pattern", "hello world", "Hello World", "Hello World", "should convert to ucwords pattern"}, + {"ucwords with symbols", "hello-world", "Hello-World", "Hello-World", "should handle ucwords with symbols"}, + {"ucwords with multiple words", "hello brave new world", "This Is A Test", "Hello Brave New World", "should handle multiple words in ucwords pattern"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := MatchCase(test.word, test.comparison) + assert.Equal(t, test.expected, result, "Input: %s, Comparison: %s - %s", test.word, test.comparison, test.description) + }) + } +} + +func TestUcFirst(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"word", "Word"}, + {"Word", "Word"}, + {"wORD", "Word"}, + {"123word", "123Word"}, + {"!@#word", "!@#Word"}, + {"", ""}, + {"a", "A"}, + {"A", "A"}, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + result := UcFirst(test.input) + assert.Equal(t, test.expected, result) + }) + } +} + +func TestUcWords(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"hello world", "Hello World"}, + {"HELLO WORLD", "Hello World"}, + {"hElLo WoRLd", "Hello World"}, + {"hello-world", "Hello-World"}, + {"hello_world", "Hello_World"}, + {"hello123 world456", "Hello123 World456"}, + {"", ""}, + {"a b c", "A B C"}, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + result := UcWords(test.input) + assert.Equal(t, test.expected, result) + }) + } +} diff --git a/support/pluralizer/init.go b/support/pluralizer/init.go new file mode 100644 index 000000000..636960701 --- /dev/null +++ b/support/pluralizer/init.go @@ -0,0 +1,126 @@ +package pluralizer + +import ( + "github.com/goravel/framework/contracts/support/pluralizer" + "github.com/goravel/framework/errors" + "github.com/goravel/framework/support/pluralizer/english" + "github.com/goravel/framework/support/pluralizer/inflector" + "github.com/goravel/framework/support/pluralizer/rules" +) + +var ( + instance pluralizer.Inflector + defaultLanguage = LanguageEnglish + inflectorFactory = map[string]pluralizer.Inflector{ + "english": inflector.New(english.New()), + } +) + +func init() { + instance = inflectorFactory[defaultLanguage] +} + +func UseLanguage(lang string) error { + if factory, exists := inflectorFactory[lang]; exists { + instance = factory + return nil + } + return errors.PluralizerLanguageNotFound.Args(lang) +} + +func GetLanguage() pluralizer.Language { + return instance.Language() +} + +func RegisterLanguage(language pluralizer.Language) error { + if language == nil || language.Name() == "" { + return errors.PluralizerEmptyLanguageName + } + + inflectorFactory[language.Name()] = inflector.New(language) + return nil +} + +func getLanguageInstance(lang string) (pluralizer.Language, pluralizer.Inflector, bool) { + factory, exists := inflectorFactory[lang] + if !exists { + return nil, nil, false + } + + language := factory.Language() + return language, factory, true +} + +func RegisterIrregular(lang string, substitutions ...pluralizer.Substitution) error { + if len(substitutions) == 0 { + return errors.PluralizerNoSubstitutionsGiven + } + + language, factory, exists := getLanguageInstance(lang) + if !exists { + return errors.PluralizerLanguageNotFound.Args(lang) + } + + language.PluralRuleset().AddIrregular(substitutions...) + + flipped := rules.GetFlippedSubstitutions(substitutions...) + language.SingularRuleset().AddIrregular(flipped...) + + factory.SetLanguage(language) + return nil +} + +func RegisterUninflected(lang string, words ...string) error { + if len(words) == 0 { + return errors.PluralizerNoWordsGiven + } + + language, factory, exists := getLanguageInstance(lang) + if !exists { + return errors.PluralizerLanguageNotFound.Args(lang) + } + + language.PluralRuleset().AddUninflected(words...) + language.SingularRuleset().AddUninflected(words...) + + factory.SetLanguage(language) + return nil +} + +func RegisterPluralUninflected(lang string, words ...string) error { + if len(words) == 0 { + return errors.PluralizerNoWordsGiven + } + + language, factory, exists := getLanguageInstance(lang) + if !exists { + return errors.PluralizerLanguageNotFound.Args(lang) + } + + language.PluralRuleset().AddUninflected(words...) + factory.SetLanguage(language) + return nil +} + +func RegisterSingularUninflected(lang string, words ...string) error { + if len(words) == 0 { + return errors.PluralizerNoWordsGiven + } + + language, factory, exists := getLanguageInstance(lang) + if !exists { + return errors.PluralizerLanguageNotFound.Args(lang) + } + + language.SingularRuleset().AddUninflected(words...) + factory.SetLanguage(language) + return nil +} + +func Plural(word string) string { + return instance.Plural(word) +} + +func Singular(word string) string { + return instance.Singular(word) +} diff --git a/support/pluralizer/init_test.go b/support/pluralizer/init_test.go new file mode 100644 index 000000000..5babf639f --- /dev/null +++ b/support/pluralizer/init_test.go @@ -0,0 +1,223 @@ +package pluralizer + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/goravel/framework/contracts/support/pluralizer" + "github.com/goravel/framework/errors" + "github.com/goravel/framework/support/pluralizer/rules" +) + +func TestDefaultLanguage(t *testing.T) { + assert.Equal(t, LanguageEnglish, GetLanguage().Name()) +} + +func TestUseLanguage(t *testing.T) { + originalLang := GetLanguage().Name() + defer func() { + assert.Nil(t, UseLanguage(originalLang)) + }() + + err := UseLanguage(LanguageEnglish) + assert.Nil(t, err) + assert.Equal(t, LanguageEnglish, GetLanguage().Name()) + + err = UseLanguage("nonexistent") + assert.NotNil(t, err) + assert.Equal(t, LanguageEnglish, GetLanguage().Name()) + assert.ErrorIs(t, err, errors.PluralizerLanguageNotFound) +} + +func TestRegisterLanguage(t *testing.T) { + originalLang := GetLanguage().Name() + defer func() { + assert.Nil(t, UseLanguage(originalLang)) + }() + + mockLang := newMockLanguage("test") + err := RegisterLanguage(mockLang) + assert.Nil(t, err) + + err = UseLanguage("test") + assert.Nil(t, err) + assert.Equal(t, "test", GetLanguage().Name()) + + err = RegisterLanguage(nil) + assert.NotNil(t, err) + assert.ErrorIs(t, err, errors.PluralizerEmptyLanguageName) + + emptyLang := newMockLanguage("") + err = RegisterLanguage(emptyLang) + assert.NotNil(t, err) + assert.ErrorIs(t, err, errors.PluralizerEmptyLanguageName) +} + +func TestRegisterIrregular(t *testing.T) { + sub1 := rules.NewSubstitution("test", "tests") + sub2 := rules.NewSubstitution("exam", "exams") + + err := RegisterIrregular(LanguageEnglish, sub1, sub2) + assert.Nil(t, err) + + assert.Equal(t, "tests", Plural("test")) + assert.Equal(t, "test", Singular("tests")) + assert.Equal(t, "exams", Plural("exam")) + assert.Equal(t, "exam", Singular("exams")) + + err = RegisterIrregular("nonexistent", sub1) + assert.NotNil(t, err) + assert.ErrorIs(t, err, errors.PluralizerLanguageNotFound) + + err = RegisterIrregular(LanguageEnglish) + assert.NotNil(t, err) + assert.ErrorIs(t, err, errors.PluralizerNoSubstitutionsGiven) +} + +func TestRegisterUninflected(t *testing.T) { + err := RegisterUninflected(LanguageEnglish, "testdata", "metadata") + assert.Nil(t, err) + + assert.Equal(t, "testdata", Plural("testdata")) + assert.Equal(t, "testdata", Singular("testdata")) + assert.Equal(t, "metadata", Plural("metadata")) + assert.Equal(t, "metadata", Singular("metadata")) + + err = RegisterUninflected("nonexistent", "data") + assert.NotNil(t, err) + assert.ErrorIs(t, err, errors.PluralizerLanguageNotFound) + + err = RegisterUninflected(LanguageEnglish) + assert.NotNil(t, err) + assert.ErrorIs(t, err, errors.PluralizerNoWordsGiven) +} + +func TestRegisterPluralUninflected(t *testing.T) { + err := RegisterPluralUninflected(LanguageEnglish, "pluraldata") + assert.Nil(t, err) + + assert.Equal(t, "pluraldata", Plural("pluraldata")) + + err = RegisterPluralUninflected("nonexistent", "data") + assert.NotNil(t, err) + assert.ErrorIs(t, err, errors.PluralizerLanguageNotFound) + + err = RegisterPluralUninflected(LanguageEnglish) + assert.NotNil(t, err) + assert.ErrorIs(t, err, errors.PluralizerNoWordsGiven) +} + +func TestRegisterSingularUninflected(t *testing.T) { + err := RegisterSingularUninflected(LanguageEnglish, "singulardata") + assert.Nil(t, err) + assert.Equal(t, "singulardata", Singular("singulardata")) + + err = RegisterSingularUninflected("nonexistent", "data") + assert.NotNil(t, err) + assert.ErrorIs(t, err, errors.PluralizerLanguageNotFound) + + err = RegisterSingularUninflected(LanguageEnglish) + assert.NotNil(t, err) + assert.ErrorIs(t, err, errors.PluralizerNoWordsGiven) +} + +func TestGlobalPluralFunction(t *testing.T) { + result := Plural("book") + assert.Equal(t, "books", result) +} + +func TestGlobalSingularFunction(t *testing.T) { + result := Singular("books") + assert.Equal(t, "book", result) +} + +func TestComplexWorkflow(t *testing.T) { + originalLang := GetLanguage().Name() + defer func() { + assert.Nil(t, UseLanguage(originalLang)) + }() + + testLang := newMockLanguage("testlang") + err := RegisterLanguage(testLang) + assert.Nil(t, err) + + err = UseLanguage("testlang") + assert.Nil(t, err) + assert.Equal(t, "testlang", GetLanguage().Name()) + + err = RegisterIrregular("testlang", rules.NewSubstitution("testword", "testwords")) + assert.Nil(t, err) + + err = RegisterUninflected("testlang", "staticword") + assert.Nil(t, err) + + assert.Equal(t, "testwords", Plural("testword")) + assert.Equal(t, "testword", Singular("testwords")) + assert.Equal(t, "staticword", Plural("staticword")) + assert.Equal(t, "staticword", Singular("staticword")) + + err = UseLanguage(LanguageEnglish) + assert.Nil(t, err) + + err = RegisterIrregular(LanguageEnglish, rules.NewSubstitution("workflowtest", "workflowtests")) + assert.Nil(t, err) + + assert.Equal(t, "workflowtests", Plural("workflowtest")) + assert.Equal(t, "workflowtest", Singular("workflowtests")) + assert.Equal(t, "books", Plural("book")) +} + +func TestEdgeCases(t *testing.T) { + result := Plural("") + assert.Equal(t, "", result) + + result = Singular("") + assert.Equal(t, "", result) + result = Plural("Book") + assert.Equal(t, "Books", result) + + result = Plural("test-case") + assert.NotEqual(t, "", result) +} + +func TestErrorReturns(t *testing.T) { + assert.Nil(t, UseLanguage(LanguageEnglish)) + assert.Nil(t, RegisterLanguage(newMockLanguage("testreturn"))) + assert.Nil(t, RegisterIrregular(LanguageEnglish, rules.NewSubstitution("a", "as"))) + assert.Nil(t, RegisterUninflected(LanguageEnglish, "testword")) + assert.Nil(t, RegisterPluralUninflected(LanguageEnglish, "testword2")) + assert.Nil(t, RegisterSingularUninflected(LanguageEnglish, "testword3")) + + assert.NotNil(t, UseLanguage("nonexistent")) + assert.NotNil(t, RegisterLanguage(nil)) + assert.NotNil(t, RegisterIrregular("nonexistent", rules.NewSubstitution("a", "as"))) + assert.NotNil(t, RegisterUninflected("nonexistent", "testword")) + assert.NotNil(t, RegisterPluralUninflected(LanguageEnglish)) +} + +type mockLanguage struct { + name string + pluralRuleset pluralizer.Ruleset + singularRuleset pluralizer.Ruleset +} + +func newMockLanguage(name string) *mockLanguage { + return &mockLanguage{ + name: name, + pluralRuleset: rules.NewRuleset(nil, nil, nil), + singularRuleset: rules.NewRuleset(nil, nil, nil), + } +} + +func (m *mockLanguage) Name() string { + return m.name +} + +func (m *mockLanguage) PluralRuleset() pluralizer.Ruleset { + return m.pluralRuleset +} + +func (m *mockLanguage) SingularRuleset() pluralizer.Ruleset { + return m.singularRuleset +} diff --git a/support/pluralizer/language.go b/support/pluralizer/language.go new file mode 100644 index 000000000..c0857fdab --- /dev/null +++ b/support/pluralizer/language.go @@ -0,0 +1,5 @@ +package pluralizer + +const ( + LanguageEnglish = "english" +) diff --git a/support/pluralizer/rules/pattern.go b/support/pluralizer/rules/pattern.go new file mode 100644 index 000000000..84c923f39 --- /dev/null +++ b/support/pluralizer/rules/pattern.go @@ -0,0 +1,22 @@ +package rules + +import ( + "github.com/goravel/framework/contracts/support/pluralizer" + "regexp" +) + +var _ pluralizer.Pattern = (*Pattern)(nil) + +type Pattern struct { + pattern *regexp.Regexp +} + +func NewPattern(pattern string) *Pattern { + return &Pattern{ + pattern: regexp.MustCompile(pattern), + } +} + +func (r *Pattern) Matches(word string) bool { + return r.pattern.MatchString(word) +} diff --git a/support/pluralizer/rules/ruleset.go b/support/pluralizer/rules/ruleset.go new file mode 100644 index 000000000..60b4527e0 --- /dev/null +++ b/support/pluralizer/rules/ruleset.go @@ -0,0 +1,50 @@ +package rules + +import "github.com/goravel/framework/contracts/support/pluralizer" + +var _ pluralizer.Ruleset = (*Ruleset)(nil) + +type Ruleset struct { + regular pluralizer.Transformations + uninflected pluralizer.Patterns + irregular pluralizer.Substitutions +} + +func NewRuleset(regular pluralizer.Transformations, uninflected pluralizer.Patterns, irregular pluralizer.Substitutions) *Ruleset { + return &Ruleset{ + regular: regular, + uninflected: uninflected, + irregular: irregular, + } +} + +func (r *Ruleset) AddIrregular(substitutions ...pluralizer.Substitution) pluralizer.Ruleset { + r.irregular = append(substitutions, r.irregular...) + return r +} + +func (r *Ruleset) AddUninflected(words ...string) pluralizer.Ruleset { + if len(words) == 0 { + return r + } + + patterns := make([]pluralizer.Pattern, len(words)) + for i, word := range words { + patterns[i] = NewPattern(word) + } + + r.uninflected = append(patterns, r.uninflected...) + return r +} + +func (r *Ruleset) Regular() pluralizer.Transformations { + return r.regular +} + +func (r *Ruleset) Uninflected() pluralizer.Patterns { + return r.uninflected +} + +func (r *Ruleset) Irregular() pluralizer.Substitutions { + return r.irregular +} diff --git a/support/pluralizer/rules/substitution.go b/support/pluralizer/rules/substitution.go new file mode 100644 index 000000000..e5eb13325 --- /dev/null +++ b/support/pluralizer/rules/substitution.go @@ -0,0 +1,36 @@ +package rules + +import ( + "github.com/goravel/framework/contracts/support/pluralizer" +) + +var _ pluralizer.Substitution = (*Substitution)(nil) + +type Substitution struct { + from string + to string +} + +func NewSubstitution(from, to string) *Substitution { + return &Substitution{ + from: from, + to: to, + } +} + +func (r *Substitution) From() string { + return r.from +} + +func (r *Substitution) To() string { + return r.to +} + +func GetFlippedSubstitutions(substitutions ...pluralizer.Substitution) []pluralizer.Substitution { + flipped := make([]pluralizer.Substitution, len(substitutions)) + for i, sub := range substitutions { + flipped[i] = NewSubstitution(sub.To(), sub.From()) + } + + return flipped +} diff --git a/support/pluralizer/rules/transformation.go b/support/pluralizer/rules/transformation.go new file mode 100644 index 000000000..d33782c1e --- /dev/null +++ b/support/pluralizer/rules/transformation.go @@ -0,0 +1,29 @@ +package rules + +import ( + "regexp" + + "github.com/goravel/framework/contracts/support/pluralizer" +) + +type Transformation struct { + pattern *regexp.Regexp + replacement string +} + +var _ pluralizer.Transformation = (*Transformation)(nil) + +func NewTransformation(pattern, replacement string) *Transformation { + return &Transformation{ + pattern: regexp.MustCompile(pattern), + replacement: replacement, + } +} + +func (r *Transformation) Apply(word string) string { + if !r.pattern.MatchString(word) { + return "" + } + + return r.pattern.ReplaceAllString(word, r.replacement) +} diff --git a/support/str/str.go b/support/str/str.go index 599579048..f51f35ea4 100644 --- a/support/str/str.go +++ b/support/str/str.go @@ -11,6 +11,8 @@ import ( "golang.org/x/text/cases" "golang.org/x/text/language" + + "github.com/goravel/framework/support/pluralizer" ) type String struct { @@ -530,6 +532,17 @@ func (s *String) Pipe(callback func(s string) string) *String { return s } +// Plural returns the plural form of the string. +// If count is provided and equals 1, returns the singular form, otherwise returns the plural form. +func (s *String) Plural(count ...int) *String { + if len(count) > 0 && count[0] == 1 { + s.value = pluralizer.Singular(s.value) + } else { + s.value = pluralizer.Plural(s.value) + } + return s +} + // Prepend one or more strings to the current string. func (s *String) Prepend(values ...string) *String { s.value = strings.Join(values, "") + s.value @@ -629,6 +642,12 @@ func (s *String) RTrim(characters ...string) *String { return s } +// Singular returns the singular form of the string. +func (s *String) Singular() *String { + s.value = pluralizer.Singular(s.value) + return s +} + // Snake returns the String instance in snake case. func (s *String) Snake(delimiter ...string) *String { defaultDelimiter := "_" diff --git a/support/str/str_test.go b/support/str/str_test.go index eb71d3918..8e02d96bc 100644 --- a/support/str/str_test.go +++ b/support/str/str_test.go @@ -1046,6 +1046,58 @@ func (s *StringTestSuite) TestWords() { s.Equal("Perfectly balanced, as all things should be.", Of("Perfectly balanced, as all things should be.").Words(100).String()) } +func (s *StringTestSuite) TestPlural() { + // Basic pluralization + s.Equal("books", Of("book").Plural().String()) + s.Equal("people", Of("person").Plural().String()) + s.Equal("children", Of("child").Plural().String()) + s.Equal("geese", Of("goose").Plural().String()) + s.Equal("mice", Of("mouse").Plural().String()) + s.Equal("oxen", Of("ox").Plural().String()) + s.Equal("leaves", Of("leaf").Plural().String()) + s.Equal("feet", Of("foot").Plural().String()) + s.Equal("teeth", Of("tooth").Plural().String()) + + // With count parameter + s.Equal("book", Of("book").Plural(1).String()) + s.Equal("books", Of("book").Plural(2).String()) + s.Equal("person", Of("person").Plural(1).String()) + s.Equal("people", Of("person").Plural(2).String()) + + // Uncountable words + s.Equal("fish", Of("fish").Plural().String()) + s.Equal("sheep", Of("sheep").Plural().String()) + s.Equal("deer", Of("deer").Plural().String()) + s.Equal("information", Of("information").Plural().String()) + + // Case preservation + s.Equal("Books", Of("Book").Plural().String()) + s.Equal("BOOKS", Of("BOOK").Plural().String()) +} + +func (s *StringTestSuite) TestSingular() { + // Basic singularization + s.Equal("book", Of("books").Singular().String()) + s.Equal("person", Of("people").Singular().String()) + s.Equal("child", Of("children").Singular().String()) + s.Equal("goose", Of("geese").Singular().String()) + s.Equal("mouse", Of("mice").Singular().String()) + s.Equal("ox", Of("oxen").Singular().String()) + s.Equal("leaf", Of("leaves").Singular().String()) + s.Equal("foot", Of("feet").Singular().String()) + s.Equal("tooth", Of("teeth").Singular().String()) + + // Uncountable words + s.Equal("fish", Of("fish").Singular().String()) + s.Equal("sheep", Of("sheep").Singular().String()) + s.Equal("deer", Of("deer").Singular().String()) + s.Equal("information", Of("information").Singular().String()) + + // Case preservation + s.Equal("Book", Of("Books").Singular().String()) + s.Equal("BOOK", Of("BOOKS").Singular().String()) +} + func TestFieldsFunc(t *testing.T) { tests := []struct { input string