Skip to content

Commit d5c8413

Browse files
committed
Allow role refs to have an entrypoint.
1 parent 1051c18 commit d5c8413

File tree

5 files changed

+85
-5
lines changed

5 files changed

+85
-5
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
major_changes:
2+
- "Extend plugin reference ``P(...)`` to allow referencing a role's entrypoint (https://github.com/ansible-community/antsibull-docs-ts/pull/442)."

src/ansible.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export function isFQCN(input: string): boolean {
88
return input.match(/^[A-Za-z0-9_]+\.[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/) !== null;
99
}
1010

11+
export function isEntrypoint(input: string): boolean {
12+
return input.match(/^[A-Za-z0-9_]+$/) !== null;
13+
}
14+
1115
export function isPluginType(input: string): boolean {
1216
/* We do not want to hard-code a list of valid plugin types that might be inaccurate, so we simply check whether this is a valid kind of Python identifier usually used for plugin types. If ansible-core ever adds one with digits, we'll have to update this. */
1317
return /^[a-z_]+$/.test(input);

src/dom.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export interface ModulePart extends Part {
5252
export interface PluginPart extends Part {
5353
type: PartType.PLUGIN;
5454
plugin: PluginIdentifier;
55+
entrypoint: string | undefined; // can be present if plugin.type == 'role'
5556
}
5657

5758
export interface URLPart extends Part {

src/parser.spec.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,12 @@ describe('parser', (): void => {
131131
{ type: PartType.TEXT, text: 'foo ', source: undefined },
132132
{ type: PartType.ENV_VARIABLE, name: 'a),b', source: undefined },
133133
{ type: PartType.TEXT, text: ' ', source: undefined },
134-
{ type: PartType.PLUGIN, plugin: { fqcn: 'foo.bar.baz', type: 'bam' }, source: undefined },
134+
{
135+
type: PartType.PLUGIN,
136+
plugin: { fqcn: 'foo.bar.baz', type: 'bam' },
137+
entrypoint: undefined,
138+
source: undefined,
139+
},
135140
{ type: PartType.TEXT, text: ' baz ', source: undefined },
136141
{ type: PartType.OPTION_VALUE, value: ' b,na)\\m, ', source: undefined },
137142
{ type: PartType.TEXT, text: ' ', source: undefined },
@@ -147,6 +152,23 @@ describe('parser', (): void => {
147152
{ type: PartType.TEXT, text: ' ', source: undefined },
148153
],
149154
]);
155+
expect(parse('P(foo.bar.baz#role) P(foo.bar.baz#role:entrypoint)')).toEqual([
156+
[
157+
{
158+
type: PartType.PLUGIN,
159+
plugin: { fqcn: 'foo.bar.baz', type: 'role' },
160+
entrypoint: undefined,
161+
source: undefined,
162+
},
163+
{ type: PartType.TEXT, text: ' ', source: undefined },
164+
{
165+
type: PartType.PLUGIN,
166+
plugin: { fqcn: 'foo.bar.baz', type: 'role' },
167+
entrypoint: 'entrypoint',
168+
source: undefined,
169+
},
170+
],
171+
]);
150172
});
151173
it('semantic markup test (with source)', (): void => {
152174
expect(parse('foo E(a\\),b) P(foo.bar.baz#bam) baz V( b\\,\\na\\)\\\\m\\, ) O(foo) ', { addSource: true })).toEqual(
@@ -155,7 +177,12 @@ describe('parser', (): void => {
155177
{ type: PartType.TEXT, text: 'foo ', source: 'foo ' },
156178
{ type: PartType.ENV_VARIABLE, name: 'a),b', source: 'E(a\\),b)' },
157179
{ type: PartType.TEXT, text: ' ', source: ' ' },
158-
{ type: PartType.PLUGIN, plugin: { fqcn: 'foo.bar.baz', type: 'bam' }, source: 'P(foo.bar.baz#bam)' },
180+
{
181+
type: PartType.PLUGIN,
182+
plugin: { fqcn: 'foo.bar.baz', type: 'bam' },
183+
entrypoint: undefined,
184+
source: 'P(foo.bar.baz#bam)',
185+
},
159186
{ type: PartType.TEXT, text: ' baz ', source: ' baz ' },
160187
{ type: PartType.OPTION_VALUE, value: ' b,na)\\m, ', source: 'V( b\\,\\na\\)\\\\m\\, )' },
161188
{ type: PartType.TEXT, text: ' ', source: ' ' },
@@ -172,6 +199,23 @@ describe('parser', (): void => {
172199
],
173200
],
174201
);
202+
expect(parse('P(foo.bar.baz#role) P(foo.bar.baz#role:entrypoint)')).toEqual([
203+
[
204+
{
205+
type: PartType.PLUGIN,
206+
plugin: { fqcn: 'foo.bar.baz', type: 'role' },
207+
entrypoint: undefined,
208+
source: undefined,
209+
},
210+
{ type: PartType.TEXT, text: ' ', source: undefined },
211+
{
212+
type: PartType.PLUGIN,
213+
plugin: { fqcn: 'foo.bar.baz', type: 'role' },
214+
entrypoint: 'entrypoint',
215+
source: undefined,
216+
},
217+
],
218+
]);
175219
});
176220
it('semantic markup option name', (): void => {
177221
expect(parse('O(foo)')).toEqual([
@@ -432,6 +476,12 @@ describe('parser', (): void => {
432476
expect(async () => parse('P(foo.bar.baz#b m)', { errors: 'exception', helpfulErrors: false })).rejects.toThrow(
433477
'While parsing P() at index 1: Plugin type "b m" is not valid',
434478
);
479+
expect(async () =>
480+
parse('P(foo.bar.baz#module:e p)', { errors: 'exception', helpfulErrors: false }),
481+
).rejects.toThrow('While parsing P() at index 1: Entrypoint "e p" is not valid');
482+
expect(async () =>
483+
parse('P(foo.bar.baz#module:entrypoint)', { errors: 'exception', helpfulErrors: false }),
484+
).rejects.toThrow('While parsing P() at index 1: Only role references can have entrypoints');
435485
});
436486
it('bad option name/return value (throw error)', (): void => {
437487
expect(async () =>
@@ -446,6 +496,9 @@ describe('parser', (): void => {
446496
expect(async () => parse('O(foo.bar.baz#role:bam)', { errors: 'exception', helpfulErrors: false })).rejects.toThrow(
447497
'While parsing O() at index 1: Role reference is missing entrypoint',
448498
);
499+
expect(async () =>
500+
parse('O(foo.bar.baz#role:e p:bam)', { errors: 'exception', helpfulErrors: false }),
501+
).rejects.toThrow('While parsing O() at index 1: Entrypoint "e p" is not valid');
449502
});
450503
it('bad parameter parsing (no escaping, error message)', (): void => {
451504
expect(parse('M(', { helpfulErrors: false })).toEqual([

src/parser.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { ParsingOptions, PluginIdentifier, Whitespace } from './opts';
8-
import { isFQCN, isPluginType } from './ansible';
8+
import { isFQCN, isPluginType, isEntrypoint } from './ansible';
99
import { parseEscapedArgs, parseUnescapedArgs } from './parser-impl';
1010
import {
1111
PartType,
@@ -72,6 +72,9 @@ function parseOptionLike(
7272
if (idx >= 0) {
7373
entrypoint = text.substr(0, idx);
7474
text = text.substr(idx + 1);
75+
if (!isEntrypoint(entrypoint)) {
76+
throw Error(`Entrypoint ${repr(entrypoint)} is not valid`);
77+
}
7578
}
7679
if (entrypoint === undefined) {
7780
throw Error('Role reference is missing entrypoint');
@@ -258,11 +261,28 @@ const PARSER: CommandParserEx[] = [
258261
if (!isFQCN(fqcn)) {
259262
throw Error(`Plugin name ${repr(fqcn)} is not a FQCN`);
260263
}
261-
const type = m[2] as string;
264+
let type = m[2] as string;
265+
let entrypoint: string | undefined;
266+
const m2 = /^([^:]+):([^:]+)$/.exec(type);
267+
if (m2) {
268+
type = m2[1] as string;
269+
entrypoint = m2[2] as string;
270+
if (!isEntrypoint(entrypoint)) {
271+
throw Error(`Entrypoint ${repr(entrypoint)} is not valid`);
272+
}
273+
}
262274
if (!isPluginType(type)) {
263275
throw Error(`Plugin type ${repr(type)} is not valid`);
264276
}
265-
return <PluginPart>{ type: PartType.PLUGIN, plugin: { fqcn: fqcn, type: type }, source: source };
277+
if (entrypoint && type !== 'role') {
278+
throw Error('Only role references can have entrypoints');
279+
}
280+
return <PluginPart>{
281+
type: PartType.PLUGIN,
282+
plugin: { fqcn: fqcn, type: type },
283+
entrypoint: entrypoint,
284+
source: source,
285+
};
266286
},
267287
},
268288
{

0 commit comments

Comments
 (0)