Skip to content

Commit a2db14e

Browse files
authored
Merge pull request #66 from constructive-io/fix/inflekt-casing-class
fix(inflekt): normalize malformed class plurals
2 parents d591a75 + 8116a3a commit a2db14e

2 files changed

Lines changed: 121 additions & 6 deletions

File tree

packages/inflekt/__tests__/inflection.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ describe('singularize', () => {
4646
expect(singularize('apiSchemata')).toBe('apiSchema');
4747
expect(singularize('SCHEMATA')).toBe('SCHEMA');
4848
});
49+
50+
it('should canonicalize malformed trailing triple-s words', () => {
51+
expect(singularize('classs')).toBe('class');
52+
expect(singularize('Classs')).toBe('Class');
53+
expect(singularize('hazardClasss')).toBe('hazardClass');
54+
expect(singularize('hazardClassses')).toBe('hazardClass');
55+
expect(singularize('CLASSS')).toBe('CLASS');
56+
});
4957
});
5058

5159
describe('pluralize', () => {
@@ -55,6 +63,36 @@ describe('pluralize', () => {
5563
expect(pluralize('Person')).toBe('People');
5664
expect(pluralize('Category')).toBe('Categories');
5765
});
66+
67+
it('should normalize class variants', () => {
68+
expect(pluralize('class')).toBe('classes');
69+
expect(pluralize('Class')).toBe('Classes');
70+
expect(pluralize('hazardClass')).toBe('hazardClasses');
71+
expect(pluralize('HazardClass')).toBe('HazardClasses');
72+
expect(pluralize('hazardClasss')).toBe('hazardClasses');
73+
expect(pluralize('classs')).toBe('classes');
74+
});
75+
76+
it('should preserve already-plural Latin words', () => {
77+
expect(pluralize('Schemata')).toBe('Schemata');
78+
expect(pluralize('schemata')).toBe('schemata');
79+
});
80+
81+
it.each([
82+
['class', 'classes'],
83+
['glass', 'glasses'],
84+
['boss', 'bosses'],
85+
['process', 'processes'],
86+
['address', 'addresses'],
87+
['witness', 'witnesses'],
88+
['abyss', 'abysses'],
89+
])(
90+
'should handle -ss noun roundtrip for %s -> %s',
91+
(singularWord, pluralWord) => {
92+
expect(pluralize(singularWord)).toBe(pluralWord);
93+
expect(singularize(pluralWord)).toBe(singularWord);
94+
}
95+
);
5896
});
5997

6098
describe('singularizeLast', () => {
@@ -69,6 +107,11 @@ describe('singularizeLast', () => {
69107
expect(singularizeLast('api_schemata')).toBe('api_schema');
70108
expect(singularizeLast('ApiSchemata')).toBe('ApiSchema');
71109
});
110+
111+
it('should normalize malformed class suffixes in the final segment', () => {
112+
expect(singularizeLast('hazardClassses')).toBe('hazardClass');
113+
expect(singularizeLast('HazardClassses')).toBe('HazardClass');
114+
});
72115
});
73116

74117
describe('pluralizeLast', () => {
@@ -78,6 +121,13 @@ describe('pluralizeLast', () => {
78121
expect(pluralizeLast('order_item')).toBe('order_items');
79122
expect(pluralizeLast('OrderItem')).toBe('OrderItems');
80123
});
124+
125+
it('should normalize malformed class suffixes in the final segment', () => {
126+
expect(pluralizeLast('hazardClass')).toBe('hazardClasses');
127+
expect(pluralizeLast('HazardClass')).toBe('HazardClasses');
128+
expect(pluralizeLast('hazardClasss')).toBe('hazardClasses');
129+
expect(pluralizeLast('HazardClasss')).toBe('HazardClasses');
130+
});
81131
});
82132

83133
describe('distinctPluralize', () => {
@@ -95,13 +145,23 @@ describe('distinctPluralize', () => {
95145
expect(distinctPluralize('bus')).toBe('buses');
96146
expect(distinctPluralize('box')).toBe('boxes');
97147
});
148+
149+
it('should normalize malformed class variants', () => {
150+
expect(distinctPluralize('classs')).toBe('classes');
151+
expect(distinctPluralize('hazardClasss')).toBe('hazardClasses');
152+
});
98153
});
99154

100155
describe('distinctPluralizeLast', () => {
101156
it('should distinctly pluralize only the last word', () => {
102157
expect(distinctPluralizeLast('user_profile')).toBe('user_profiles');
103158
expect(distinctPluralizeLast('UserProfile')).toBe('UserProfiles');
104159
});
160+
161+
it('should normalize malformed class variants in the final segment', () => {
162+
expect(distinctPluralizeLast('hazardClasss')).toBe('hazardClasses');
163+
expect(distinctPluralizeLast('HazardClasss')).toBe('HazardClasses');
164+
});
105165
});
106166

107167
describe('lcFirst', () => {
@@ -189,12 +249,20 @@ describe('toFieldName', () => {
189249
expect(toFieldName('Schemata')).toBe('schema');
190250
expect(toFieldName('ApiSchemata')).toBe('apiSchema');
191251
});
252+
253+
it('should normalize malformed class variants', () => {
254+
expect(toFieldName('Classes')).toBe('class');
255+
expect(toFieldName('HazardClasses')).toBe('hazardClass');
256+
expect(toFieldName('HazardClassses')).toBe('hazardClass');
257+
});
192258
});
193259

194260
describe('toQueryName', () => {
195261
it('should convert singular PascalCase to plural camelCase', () => {
196262
expect(toQueryName('User')).toBe('users');
197263
expect(toQueryName('OrderItem')).toBe('orderItems');
198264
expect(toQueryName('Category')).toBe('categories');
265+
expect(toQueryName('Class')).toBe('classes');
266+
expect(toQueryName('HazardClass')).toBe('hazardClasses');
199267
});
200268
});

packages/inflekt/src/pluralize.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,59 @@ const LATIN_SUFFIX_OVERRIDES: Array<[string, string]> = [
2525
['data', 'datum'],
2626
];
2727

28+
const TRAILING_TRIPLE_S_REGEX = /[sS]{3,}$/;
29+
const TRAILING_TRIPLE_S_BEFORE_ES_REGEX = /[sS]{3,}(?=e[sS]$)/;
30+
31+
function normalizeTrailingSRun(suffix: string): string {
32+
return suffix === suffix.toUpperCase() ? 'SS' : 'ss';
33+
}
34+
35+
function normalizeTripleSBeforeEs(word: string): string {
36+
return word.replace(TRAILING_TRIPLE_S_BEFORE_ES_REGEX, normalizeTrailingSRun);
37+
}
38+
39+
function normalizeTrailingTripleS(word: string): string {
40+
const match = word.match(TRAILING_TRIPLE_S_REGEX);
41+
if (!match) {
42+
return word;
43+
}
44+
45+
const suffix = match[0];
46+
const prefix = word.slice(0, -suffix.length);
47+
const normalizedSuffix = normalizeTrailingSRun(suffix);
48+
return `${prefix}${normalizedSuffix}`;
49+
}
50+
51+
function normalizeMalformedDoubleS(word: string): string {
52+
return normalizeTrailingTripleS(normalizeTripleSBeforeEs(word));
53+
}
54+
55+
function enforceDoubleSPlural(singularWord: string, pluralWord: string): string {
56+
if (!singularWord.toLowerCase().endsWith('ss')) {
57+
return pluralWord;
58+
}
59+
60+
// Defensive normalization for malformed outputs like "hazardClasss".
61+
if (pluralWord === `${singularWord}s`) {
62+
return `${singularWord}es`;
63+
}
64+
65+
return pluralWord;
66+
}
67+
2868
/**
2969
* Convert a word to its singular form with PostGraphile-compatible Latin handling
3070
* @example "Users" -> "User", "People" -> "Person", "Schemata" -> "Schema", "ApiSchemata" -> "ApiSchema"
3171
*/
3272
export function singularize(word: string): string {
33-
const lowerWord = word.toLowerCase();
73+
const normalizedWord = normalizeMalformedDoubleS(word);
74+
const lowerWord = normalizedWord.toLowerCase();
3475

3576
for (const [pluralSuffix, singularSuffix] of LATIN_SUFFIX_OVERRIDES) {
3677
if (lowerWord.endsWith(pluralSuffix)) {
37-
const suffixStart = word.length - pluralSuffix.length;
38-
const prefix = word.slice(0, suffixStart);
39-
const originalSuffix = word.slice(suffixStart);
78+
const suffixStart = normalizedWord.length - pluralSuffix.length;
79+
const prefix = normalizedWord.slice(0, suffixStart);
80+
const originalSuffix = normalizedWord.slice(suffixStart);
4081

4182
const isAllCaps = originalSuffix === originalSuffix.toUpperCase();
4283
const isUpperSuffix =
@@ -55,15 +96,21 @@ export function singularize(word: string): string {
5596
}
5697
}
5798

58-
return inflection.singularize(word);
99+
return normalizeMalformedDoubleS(inflection.singularize(normalizedWord));
100+
}
101+
102+
function pluralizeCanonical(word: string): string {
103+
const normalizedWord = normalizeMalformedDoubleS(word);
104+
const pluralWord = normalizeMalformedDoubleS(inflection.pluralize(normalizedWord));
105+
return enforceDoubleSPlural(singularize(normalizedWord), pluralWord);
59106
}
60107

61108
/**
62109
* Convert a word to its plural form
63110
* @example "User" -> "Users", "Person" -> "People"
64111
*/
65112
export function pluralize(word: string): string {
66-
return inflection.pluralize(word);
113+
return pluralizeCanonical(word);
67114
}
68115

69116
/**

0 commit comments

Comments
 (0)