Skip to content

Commit 88f8ede

Browse files
authored
refactor(tools): pf-3836 allow search by uri, hash, path (#207)
* build, tsconfig move to es2023 target * pf.getResource, hash, uri, and path index maps for search * pf.search, basic allowance for hash, uri, and path as search * resources, optimize entry lookup loops, separate responses for templates * server.search, enhance findClosest with distance and memo * tool.patternFlyDocs, zod add min to urlList * tool.searchPatternFlyDocs, recommendations for empty searches, clean up
1 parent 20faaba commit 88f8ede

20 files changed

Lines changed: 509 additions & 250 deletions

src/__tests__/__snapshots__/patternFly.getResources.test.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ exports[`getPatternFlyMcpResources should return multiple organized facets: prop
2828
"keywordsIndex",
2929
"keywordsMap",
3030
"pathIndex",
31+
"uriIndex",
32+
"hashIndex",
3133
"byPath",
3234
"byUri",
3335
"byVersion",

src/__tests__/__snapshots__/patternFly.search.test.ts.snap

Lines changed: 0 additions & 34 deletions
This file was deleted.

src/__tests__/patternFly.getResources.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,27 @@ describe('getPatternFlyMcpResources', () => {
151151
it('should have a memoized property', async () => {
152152
expect(getPatternFlyMcpResources).toHaveProperty('memo');
153153
});
154+
155+
it('should have lowercased index keys', async () => {
156+
const result = await getPatternFlyMcpResources();
157+
158+
expect(Array.from(result.pathIndex.keys()).some(key => /[A-Z]/.test(key))).toBe(false);
159+
expect(Array.from(result.uriIndex.keys()).some(key => /[A-Z]/.test(key))).toBe(false);
160+
expect(Array.from(result.hashIndex.keys()).some(key => /[A-Z]/.test(key))).toBe(false);
161+
});
162+
163+
it('should generate unique hash IDs for pathless component entries', async () => {
164+
const result = await getPatternFlyMcpResources();
165+
const entries = Array.from(result.resources.values()).flatMap(resource => resource.entries);
166+
const ids = entries.map(entry => entry.id);
167+
const pathlessEntries = entries.filter(entry => !entry.path);
168+
169+
// Confirm that IDs generally exist.
170+
expect(ids.length).toBeGreaterThan(0);
171+
172+
const pathlessEntryIds = new Set(pathlessEntries.map(entry => entry.id));
173+
174+
// Confirm that all pathless entries have unique IDs.
175+
expect(pathlessEntryIds.size).toBe(pathlessEntries.length);
176+
});
154177
});
Lines changed: 157 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,83 @@
11
import { filterPatternFly, searchPatternFly } from '../patternFly.search';
22

33
describe('filterPatternFly', () => {
4+
const mockResources = new Map([
5+
['button', {
6+
name: 'button',
7+
groupId: 'button-group-id',
8+
entries: [
9+
{ id: 'btn-v6-react', name: 'button', version: 'v6', section: 'components', category: 'action', groupId: 'button-group-id' },
10+
{ id: 'btn-v5-react', name: 'button', version: 'v5', section: 'components', category: 'action', groupId: 'button-group-id' }
11+
],
12+
versions: {
13+
v6: {
14+
isSchemasAvailable: true,
15+
uri: 'patternfly://docs/button?version=v6',
16+
uriSchemas: 'patternfly://schemas/button?version=v6',
17+
uriSchemasId: 'button-group-id'
18+
},
19+
v5: {
20+
isSchemasAvailable: false,
21+
uri: 'patternfly://docs/button?version=v5'
22+
}
23+
}
24+
}],
25+
['modal', {
26+
name: 'modal',
27+
entries: [
28+
{ name: 'modal', section: 'components', category: 'view', version: 'v6' }
29+
]
30+
}]
31+
]);
32+
433
it.each([
534
{
6-
description: 'all filter',
7-
filters: undefined
35+
description: 'all entries, undefined',
36+
filters: undefined,
37+
expectedNames: ['button', 'button', 'modal']
38+
},
39+
{
40+
description: 'all entries, empty object',
41+
filters: {},
42+
expectedNames: ['button', 'button', 'modal']
43+
},
44+
{
45+
description: 'by version',
46+
filters: { version: 'v5' },
47+
expectedNames: ['button']
48+
},
49+
{
50+
description: 'name, button',
51+
filters: { name: 'button' },
52+
expectedNames: ['button', 'button']
853
},
954
{
10-
description: 'all filter empty object',
11-
filters: {}
55+
description: 'name, modal',
56+
filters: { name: 'modal' },
57+
expectedNames: ['modal']
1258
},
1359
{
14-
description: 'all filter empty object',
15-
filters: { version: 'v5' }
60+
description: 'name, hash',
61+
filters: { name: 'btn-v6-react' },
62+
expectedNames: ['button']
1663
},
1764
{
1865
description: 'section, components',
19-
filters: { section: 'components' }
66+
filters: { section: 'components' },
67+
expectedNames: ['button', 'button', 'modal']
2068
},
2169
{
22-
description: 'category, accessibility',
23-
filters: { category: 'accessibility' }
70+
description: 'category, action',
71+
filters: { category: 'action' },
72+
expectedNames: ['button', 'button']
2473
}
25-
])('should attempt to return filtered results, $description', async ({ filters }) => {
26-
const result = await filterPatternFly(filters as any);
74+
])('should return filtered results, $description', async ({ filters, expectedNames }) => {
75+
const result = await filterPatternFly(filters as any, mockResources as any);
2776

28-
expect(result.byEntry.length).toBeGreaterThanOrEqual(0);
29-
expect(Array.from(result.byResource).length).toBeGreaterThanOrEqual(0);
77+
expect(result.byEntry.map(result => result.name)).toEqual(expectedNames);
3078
});
3179

32-
it('should attempt to filter number results', async () => {
80+
it('should filter number results', async () => {
3381
const result = await filterPatternFly(
3482
{ section: 1 } as any,
3583
new Map([['loremIpsum', { entries: [{ section: 1 }, { section: 'dolor' }] }]]) as any
@@ -41,77 +89,125 @@ describe('filterPatternFly', () => {
4189
});
4290

4391
describe('searchPatternFly', () => {
92+
const mockMcpResources = {
93+
resources: new Map([
94+
['button', {
95+
name: 'button',
96+
groupId: 'btn-group',
97+
entries: [
98+
{ id: 'btn-v6-hash', name: 'button', version: 'v6', section: 'components', category: 'action', groupId: 'btn-group' },
99+
{ id: 'btn-v5-hash', name: 'button', version: 'v5', section: 'components', category: 'action', groupId: 'btn-group' }
100+
],
101+
versions: {
102+
v6: { uri: 'patternfly://docs/button?version=v6', isSchemasAvailable: true },
103+
v5: { uri: 'patternfly://docs/button?version=v5', isSchemasAvailable: false }
104+
}
105+
}],
106+
['modal', {
107+
name: 'modal',
108+
groupId: 'mdl-group',
109+
entries: [{ id: 'mdl-v6-hash', name: 'modal', version: 'v6', section: 'components', category: 'view', groupId: 'mdl-group' }],
110+
versions: { v6: { uri: 'patternfly://docs/modal?version=v6', isSchemasAvailable: true } }
111+
}]
112+
]),
113+
keywordsIndex: [
114+
'button',
115+
'modal',
116+
'btn-v6-hash',
117+
'mdl-v6-hash',
118+
'patternfly://docs/button',
119+
'patternfly://docs/modal'
120+
],
121+
keywordsMap: new Map([
122+
['button', new Map([['v6', ['button']], ['v5', ['button']]])],
123+
['modal', new Map([['v6', ['modal']]])],
124+
['btn-v6-hash', new Map([['v6', ['button']]])],
125+
['mdl-v6-hash', new Map([['v6', ['modal']]])],
126+
['patternfly://docs/button', new Map([['v6', ['button']], ['v5', ['button']]])],
127+
['patternfly://docs/modal', new Map([['v6', ['modal']]])]
128+
]),
129+
latestVersion: 'v6'
130+
};
131+
132+
const mockOptions = { mcpResources: Promise.resolve(mockMcpResources) as any };
133+
44134
it.each([
45135
{
46-
description: 'wildcard search',
47-
search: '*'
136+
description: 'exact match',
137+
search: 'button',
138+
expectedLength: 1,
139+
expectedName: 'button',
140+
expectedType: 'exact'
48141
},
49142
{
50-
description: 'all search',
51-
search: 'all'
143+
description: 'partial prefix',
144+
search: 'but',
145+
expectedLength: 1,
146+
expectedName: 'button',
147+
expectedType: 'prefix'
52148
},
53149
{
54-
description: 'empty all search',
55-
search: ''
56-
}
57-
])('should attempt to return an array of all available results, $description', async ({ search }) => {
58-
const { searchResults, ...rest } = await searchPatternFly(search, undefined, { allowWildCardAll: true });
59-
60-
expect(searchResults.length).toBeGreaterThan(0);
61-
expect(Object.keys(rest)).toMatchSnapshot('keys');
62-
});
63-
64-
it.each([
150+
description: 'partial suffix',
151+
search: 'ton',
152+
expectedLength: 1,
153+
expectedName: 'button',
154+
expectedType: 'suffix'
155+
},
65156
{
66-
description: 'exact match',
67-
search: 'react',
68-
matchType: 'exact'
157+
description: 'partial contains',
158+
search: 'utto',
159+
expectedLength: 1,
160+
expectedName: 'button',
161+
expectedType: 'contains'
69162
},
70163
{
71-
description: 'partial prefix match',
72-
search: 're',
73-
matchType: 'prefix'
164+
description: 'patternfly:// URI',
165+
search: 'patternfly://docs/modal',
166+
expectedLength: 1,
167+
expectedName: 'modal',
168+
expectedType: 'exact'
74169
},
75170
{
76-
description: 'partial suffix match',
77-
search: 'act',
78-
matchType: 'suffix'
171+
description: 'hash entry id without filter',
172+
search: 'btn-v6-hash',
173+
options: {},
174+
expectedLength: 2,
175+
expectedName: 'button',
176+
expectedType: 'exact'
79177
},
80178
{
81-
description: 'partial contains match',
82-
search: 'eac',
83-
matchType: 'contains'
179+
description: 'version filter',
180+
search: 'button',
181+
filters: { version: 'v5' },
182+
expectedLength: 1,
183+
expectedName: 'button',
184+
expectedType: 'exact'
84185
}
85-
])('should attempt to match components and keywords, $description', async ({ search, matchType }) => {
86-
const { searchResults } = await searchPatternFly(search);
186+
])('should return search results, $description', async ({ search, filters, options, expectedLength, expectedName, expectedType }) => {
187+
const { searchResults } = await searchPatternFly(search, { ...filters }, { ...options, ...mockOptions });
87188

88-
expect(searchResults.find(({ matchType: returnMatchType }) => returnMatchType === matchType)).toEqual(expect.objectContaining({
89-
query: expect.stringMatching(search)
90-
}));
189+
expect(searchResults?.length).toBe(expectedLength);
190+
expect(searchResults?.[0]?.matchType).toBe(expectedType);
191+
expect(searchResults?.[0]?.name).toBe(expectedName);
91192
});
92193

93194
it.each([
94195
{
95-
description: 'version',
96-
search: 'about modal',
97-
filters: { version: 'v5' }
196+
description: 'wildcard search',
197+
search: '*'
98198
},
99199
{
100-
description: 'section',
101-
search: 'popover',
102-
filters: { section: 'components' }
200+
description: 'all search',
201+
search: 'all'
103202
},
104203
{
105-
description: 'category',
106-
search: '*',
107-
filters: { category: 'grammar' },
108-
options: { allowWildCardAll: true }
204+
description: 'empty all search',
205+
search: ''
109206
}
110-
])('should allow filtering, $description', async ({ search, filters, options }) => {
111-
const { searchResults, totalResults, totalPotentialMatches } = await searchPatternFly(search, filters, options || {});
207+
])('should return an array of all available results, $description', async ({ search }) => {
208+
const { searchResults } = await searchPatternFly(search, undefined, { allowWildCardAll: true, ...mockOptions });
112209

113-
expect(searchResults.length).toBeGreaterThanOrEqual(0);
114-
expect(totalResults).toBeGreaterThanOrEqual(searchResults.length);
115-
expect(totalPotentialMatches).toBeGreaterThanOrEqual(totalResults);
210+
expect(searchResults?.length).toBe(2);
211+
expect(searchResults?.[0]?.matchType).toBe('all');
116212
});
117213
});

src/__tests__/resource.patternFlyComponentsIndex.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe('resourceCallback', () => {
5555
variables: {
5656
category: 'accessibility'
5757
},
58-
expected: '?category=accessibility'
58+
expected: 'category=accessibility'
5959
}
6060
])('should return context content, $description', async ({ variables, expected }) => {
6161
const result = await resourceCallback(undefined as any, variables);

src/__tests__/resource.patternFlyDocsIndex.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,22 +211,22 @@ describe('resourceCallback', () => {
211211
variables: {
212212
category: 'accessibility'
213213
},
214-
expected: '?category=accessibility'
214+
expected: 'category=accessibility'
215215
},
216216
{
217217
description: 'section',
218218
variables: {
219219
section: 'components'
220220
},
221-
expected: '?section=components'
221+
expected: 'section=components'
222222
},
223223
{
224224
description: 'category and section',
225225
variables: {
226226
category: 'accessibility',
227227
section: 'components'
228228
},
229-
expected: '?category=accessibility&section=components'
229+
expected: 'category=accessibility&section=components'
230230
}
231231
])('should return context content, $description', async ({ variables, expected }) => {
232232
const result = await resourceCallback(undefined as any, variables);

src/__tests__/resource.patternFlySchemasTemplate.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ describe('resourceCallback', () => {
7777
name: 'button',
7878
version: 'v6'
7979
}
80+
},
81+
{
82+
description: 'with hashed button name',
83+
variables: {
84+
name: 'ffcfb1b9b852a17ccb5b2adc12e3edd4a4ee41cb',
85+
version: 'v6'
86+
}
8087
}
8188
])('should attempt to return resource content, $description', async ({ variables }) => {
8289
const mockContent = '$schema';

0 commit comments

Comments
 (0)