Path: blob/main/src/vs/workbench/contrib/mcp/common/uriTemplate.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45export interface IUriTemplateVariable {6readonly explodable: boolean;7readonly name: string;8readonly optional: boolean;9readonly prefixLength?: number;10readonly repeatable: boolean;11}1213interface IUriTemplateComponent {14readonly expression: string;15readonly operator: string;16readonly variables: readonly IUriTemplateVariable[];17}1819/**20* Represents an RFC 6570 URI Template.21*/22export class UriTemplate {23/**24* The parsed template components (expressions).25*/26public readonly components: ReadonlyArray<IUriTemplateComponent | string>;2728private constructor(29public readonly template: string,30components: ReadonlyArray<IUriTemplateComponent | string>31) {32this.template = template;33this.components = components;34}3536/**37* Parses a URI template string into a UriTemplate instance.38*/39public static parse(template: string): UriTemplate {40const components: Array<IUriTemplateComponent | string> = [];41const regex = /\{([^{}]+)\}/g;42let match: RegExpExecArray | null;43let lastPos = 0;44while ((match = regex.exec(template))) {45const [expression, inner] = match;46components.push(template.slice(lastPos, match.index));47lastPos = match.index + expression.length;4849// Handle escaped braces: treat '{{' and '}}' as literals, not expressions50if (template[match.index - 1] === '{' || template[lastPos] === '}') {51components.push(inner);52continue;53}5455let operator = '';56let rest = inner;57if (rest.length > 0 && UriTemplate._isOperator(rest[0])) {58operator = rest[0];59rest = rest.slice(1);60}61const variables = rest.split(',').map((v): IUriTemplateVariable => {62let name = v;63let explodable = false;64let repeatable = false;65let prefixLength: number | undefined = undefined;66let optional = false;67if (name.endsWith('*')) {68explodable = true;69repeatable = true;70name = name.slice(0, -1);71}72const prefixMatch = name.match(/^(.*?):(\d+)$/);73if (prefixMatch) {74name = prefixMatch[1];75prefixLength = parseInt(prefixMatch[2], 10);76}77if (name.endsWith('?')) {78optional = true;79name = name.slice(0, -1);80}81return { explodable, name, optional, prefixLength, repeatable };82});83components.push({ expression, operator, variables });84}85components.push(template.slice(lastPos));8687return new UriTemplate(template, components);88}8990private static _operators = ['+', '#', '.', '/', ';', '?', '&'] as const;91private static _isOperator(ch: string): boolean {92return (UriTemplate._operators as readonly string[]).includes(ch);93}9495/**96* Resolves the template with the given variables.97*/98public resolve(variables: Record<string, unknown>): string {99let result = '';100for (const comp of this.components) {101if (typeof comp === 'string') {102result += comp;103} else {104result += this._expand(comp, variables);105}106}107return result;108}109110private _expand(comp: IUriTemplateComponent, variables: Record<string, unknown>): string {111const op = comp.operator;112const varSpecs = comp.variables;113if (varSpecs.length === 0) {114return comp.expression;115}116const vals: string[] = [];117const isNamed = op === ';' || op === '?' || op === '&';118const isReserved = op === '+' || op === '#';119const isFragment = op === '#';120const isLabel = op === '.';121const isPath = op === '/';122const isForm = op === '?';123const isFormCont = op === '&';124const isParam = op === ';';125126let prefix = '';127if (op === '+') { prefix = ''; }128else if (op === '#') { prefix = '#'; }129else if (op === '.') { prefix = '.'; }130else if (op === '/') { prefix = ''; }131else if (op === ';') { prefix = ';'; }132else if (op === '?') { prefix = '?'; }133else if (op === '&') { prefix = '&'; }134135for (const v of varSpecs) {136const value = variables[v.name];137const defined = Object.prototype.hasOwnProperty.call(variables, v.name);138if (value === undefined || value === null || (Array.isArray(value) && value.length === 0)) {139if (isParam) {140if (defined && (value === null || value === undefined)) {141vals.push(v.name);142}143continue;144}145if (isForm || isFormCont) {146if (defined) {147vals.push(UriTemplate._formPair(v.name, '', isNamed));148}149continue;150}151continue;152}153if (typeof value === 'object' && !Array.isArray(value)) {154if (v.explodable) {155const pairs: string[] = [];156for (const k in value) {157if (Object.prototype.hasOwnProperty.call(value, k)) {158const thisVal = String((value as any)[k]);159if (isParam) {160pairs.push(k + '=' + thisVal);161} else if (isForm || isFormCont) {162pairs.push(k + '=' + thisVal);163} else if (isLabel) {164pairs.push(k + '=' + thisVal);165} else if (isPath) {166pairs.push('/' + k + '=' + UriTemplate._encode(thisVal, isReserved));167} else {168pairs.push(k + '=' + UriTemplate._encode(thisVal, isReserved));169}170}171}172if (isLabel) {173vals.push(pairs.join('.'));174} else if (isPath) {175vals.push(pairs.join(''));176} else if (isParam) {177vals.push(pairs.join(';'));178} else if (isForm || isFormCont) {179vals.push(pairs.join('&'));180} else {181vals.push(pairs.join(','));182}183} else {184// Not explodable: join as k1,v1,k2,v2,... and assign to variable name185const pairs: string[] = [];186for (const k in value) {187if (Object.prototype.hasOwnProperty.call(value, k)) {188pairs.push(k);189pairs.push(String((value as any)[k]));190}191}192// For label, param, form, join as keys=semi,;,dot,.,comma,, (no encoding of , or ;)193const joined = pairs.join(',');194if (isLabel) {195vals.push(joined);196} else if (isParam || isForm || isFormCont) {197vals.push(v.name + '=' + joined);198} else {199vals.push(joined);200}201}202continue;203}204if (Array.isArray(value)) {205if (v.explodable) {206if (isLabel) {207vals.push(value.join('.'));208} else if (isPath) {209vals.push(value.map(x => '/' + UriTemplate._encode(x, isReserved)).join(''));210} else if (isParam) {211vals.push(value.map(x => v.name + '=' + String(x)).join(';'));212} else if (isForm || isFormCont) {213vals.push(value.map(x => v.name + '=' + String(x)).join('&'));214} else {215vals.push(value.map(x => UriTemplate._encode(x, isReserved)).join(','));216}217} else {218if (isLabel) {219vals.push(value.join(','));220} else if (isParam) {221vals.push(v.name + '=' + value.join(','));222} else if (isForm || isFormCont) {223vals.push(v.name + '=' + value.join(','));224} else {225vals.push(value.map(x => UriTemplate._encode(x, isReserved)).join(','));226}227}228continue;229}230let str = String(value);231if (v.prefixLength !== undefined) {232str = str.substring(0, v.prefixLength);233}234// For simple expansion, encode ! as well (not reserved)235// Only + and # are reserved236const enc = UriTemplate._encode(str, op === '+' || op === '#');237if (isParam) {238vals.push(v.name + '=' + enc);239} else if (isForm || isFormCont) {240vals.push(v.name + '=' + enc);241} else if (isLabel) {242vals.push(enc);243} else if (isPath) {244vals.push('/' + enc);245} else {246vals.push(enc);247}248}249250let joined = '';251if (isLabel) {252// Remove trailing dot for missing values253const filtered = vals.filter(v => v !== '');254joined = filtered.length ? prefix + filtered.join('.') : '';255} else if (isPath) {256// Remove empty segments for undefined/null257const filtered = vals.filter(v => v !== '');258joined = filtered.length ? filtered.join('') : '';259if (joined && !joined.startsWith('/')) {260joined = '/' + joined;261}262} else if (isParam) {263// For param, if value is empty string, just append ;name264joined = vals.length ? prefix + vals.map(v => v.replace(/=\s*$/, '')).join(';') : '';265} else if (isForm) {266joined = vals.length ? prefix + vals.join('&') : '';267} else if (isFormCont) {268joined = vals.length ? prefix + vals.join('&') : '';269} else if (isFragment) {270joined = prefix + vals.join(',');271} else if (isReserved) {272joined = vals.join(',');273} else {274joined = vals.join(',');275}276return joined;277}278279private static _encode(str: string, reserved: boolean): string {280return reserved ? encodeURI(str) : pctEncode(str);281}282283private static _formPair(k: string, v: unknown, named: boolean): string {284return named ? k + '=' + encodeURIComponent(String(v)) : encodeURIComponent(String(v));285}286}287288function pctEncode(str: string): string {289let out = '';290for (let i = 0; i < str.length; i++) {291const chr = str.charCodeAt(i);292if (293// alphanum ranges:294(chr >= 0x30 && chr <= 0x39 || chr >= 0x41 && chr <= 0x5a || chr >= 0x61 && chr <= 0x7a) ||295// unreserved characters:296(chr === 0x2d || chr === 0x2e || chr === 0x5f || chr === 0x7e)297) {298out += str[i];299} else {300out += '%' + chr.toString(16).toUpperCase();301}302}303return out;304}305306307