uriTemplate.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. // Claude-authored implementation of RFC 6570 URI Templates
  2. const MAX_TEMPLATE_LENGTH = 1000000; // 1MB
  3. const MAX_VARIABLE_LENGTH = 1000000; // 1MB
  4. const MAX_TEMPLATE_EXPRESSIONS = 10000;
  5. const MAX_REGEX_LENGTH = 1000000; // 1MB
  6. export class UriTemplate {
  7. /**
  8. * Returns true if the given string contains any URI template expressions.
  9. * A template expression is a sequence of characters enclosed in curly braces,
  10. * like {foo} or {?bar}.
  11. */
  12. static isTemplate(str) {
  13. // Look for any sequence of characters between curly braces
  14. // that isn't just whitespace
  15. return /\{[^}\s]+\}/.test(str);
  16. }
  17. static validateLength(str, max, context) {
  18. if (str.length > max) {
  19. throw new Error(`${context} exceeds maximum length of ${max} characters (got ${str.length})`);
  20. }
  21. }
  22. get variableNames() {
  23. return this.parts.flatMap(part => (typeof part === 'string' ? [] : part.names));
  24. }
  25. constructor(template) {
  26. UriTemplate.validateLength(template, MAX_TEMPLATE_LENGTH, 'Template');
  27. this.template = template;
  28. this.parts = this.parse(template);
  29. }
  30. toString() {
  31. return this.template;
  32. }
  33. parse(template) {
  34. const parts = [];
  35. let currentText = '';
  36. let i = 0;
  37. let expressionCount = 0;
  38. while (i < template.length) {
  39. if (template[i] === '{') {
  40. if (currentText) {
  41. parts.push(currentText);
  42. currentText = '';
  43. }
  44. const end = template.indexOf('}', i);
  45. if (end === -1)
  46. throw new Error('Unclosed template expression');
  47. expressionCount++;
  48. if (expressionCount > MAX_TEMPLATE_EXPRESSIONS) {
  49. throw new Error(`Template contains too many expressions (max ${MAX_TEMPLATE_EXPRESSIONS})`);
  50. }
  51. const expr = template.slice(i + 1, end);
  52. const operator = this.getOperator(expr);
  53. const exploded = expr.includes('*');
  54. const names = this.getNames(expr);
  55. const name = names[0];
  56. // Validate variable name length
  57. for (const name of names) {
  58. UriTemplate.validateLength(name, MAX_VARIABLE_LENGTH, 'Variable name');
  59. }
  60. parts.push({ name, operator, names, exploded });
  61. i = end + 1;
  62. }
  63. else {
  64. currentText += template[i];
  65. i++;
  66. }
  67. }
  68. if (currentText) {
  69. parts.push(currentText);
  70. }
  71. return parts;
  72. }
  73. getOperator(expr) {
  74. const operators = ['+', '#', '.', '/', '?', '&'];
  75. return operators.find(op => expr.startsWith(op)) || '';
  76. }
  77. getNames(expr) {
  78. const operator = this.getOperator(expr);
  79. return expr
  80. .slice(operator.length)
  81. .split(',')
  82. .map(name => name.replace('*', '').trim())
  83. .filter(name => name.length > 0);
  84. }
  85. encodeValue(value, operator) {
  86. UriTemplate.validateLength(value, MAX_VARIABLE_LENGTH, 'Variable value');
  87. if (operator === '+' || operator === '#') {
  88. return encodeURI(value);
  89. }
  90. return encodeURIComponent(value);
  91. }
  92. expandPart(part, variables) {
  93. if (part.operator === '?' || part.operator === '&') {
  94. const pairs = part.names
  95. .map(name => {
  96. const value = variables[name];
  97. if (value === undefined)
  98. return '';
  99. const encoded = Array.isArray(value)
  100. ? value.map(v => this.encodeValue(v, part.operator)).join(',')
  101. : this.encodeValue(value.toString(), part.operator);
  102. return `${name}=${encoded}`;
  103. })
  104. .filter(pair => pair.length > 0);
  105. if (pairs.length === 0)
  106. return '';
  107. const separator = part.operator === '?' ? '?' : '&';
  108. return separator + pairs.join('&');
  109. }
  110. if (part.names.length > 1) {
  111. const values = part.names.map(name => variables[name]).filter(v => v !== undefined);
  112. if (values.length === 0)
  113. return '';
  114. return values.map(v => (Array.isArray(v) ? v[0] : v)).join(',');
  115. }
  116. const value = variables[part.name];
  117. if (value === undefined)
  118. return '';
  119. const values = Array.isArray(value) ? value : [value];
  120. const encoded = values.map(v => this.encodeValue(v, part.operator));
  121. switch (part.operator) {
  122. case '':
  123. return encoded.join(',');
  124. case '+':
  125. return encoded.join(',');
  126. case '#':
  127. return '#' + encoded.join(',');
  128. case '.':
  129. return '.' + encoded.join('.');
  130. case '/':
  131. return '/' + encoded.join('/');
  132. default:
  133. return encoded.join(',');
  134. }
  135. }
  136. expand(variables) {
  137. let result = '';
  138. let hasQueryParam = false;
  139. for (const part of this.parts) {
  140. if (typeof part === 'string') {
  141. result += part;
  142. continue;
  143. }
  144. const expanded = this.expandPart(part, variables);
  145. if (!expanded)
  146. continue;
  147. // Convert ? to & if we already have a query parameter
  148. if ((part.operator === '?' || part.operator === '&') && hasQueryParam) {
  149. result += expanded.replace('?', '&');
  150. }
  151. else {
  152. result += expanded;
  153. }
  154. if (part.operator === '?' || part.operator === '&') {
  155. hasQueryParam = true;
  156. }
  157. }
  158. return result;
  159. }
  160. escapeRegExp(str) {
  161. return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  162. }
  163. partToRegExp(part) {
  164. const patterns = [];
  165. // Validate variable name length for matching
  166. for (const name of part.names) {
  167. UriTemplate.validateLength(name, MAX_VARIABLE_LENGTH, 'Variable name');
  168. }
  169. if (part.operator === '?' || part.operator === '&') {
  170. for (let i = 0; i < part.names.length; i++) {
  171. const name = part.names[i];
  172. const prefix = i === 0 ? '\\' + part.operator : '&';
  173. patterns.push({
  174. pattern: prefix + this.escapeRegExp(name) + '=([^&]+)',
  175. name
  176. });
  177. }
  178. return patterns;
  179. }
  180. let pattern;
  181. const name = part.name;
  182. switch (part.operator) {
  183. case '':
  184. pattern = part.exploded ? '([^/,]+(?:,[^/,]+)*)' : '([^/,]+)';
  185. break;
  186. case '+':
  187. case '#':
  188. pattern = '(.+)';
  189. break;
  190. case '.':
  191. pattern = '\\.([^/,]+)';
  192. break;
  193. case '/':
  194. pattern = '/' + (part.exploded ? '([^/,]+(?:,[^/,]+)*)' : '([^/,]+)');
  195. break;
  196. default:
  197. pattern = '([^/]+)';
  198. }
  199. patterns.push({ pattern, name });
  200. return patterns;
  201. }
  202. match(uri) {
  203. UriTemplate.validateLength(uri, MAX_TEMPLATE_LENGTH, 'URI');
  204. let pattern = '^';
  205. const names = [];
  206. for (const part of this.parts) {
  207. if (typeof part === 'string') {
  208. pattern += this.escapeRegExp(part);
  209. }
  210. else {
  211. const patterns = this.partToRegExp(part);
  212. for (const { pattern: partPattern, name } of patterns) {
  213. pattern += partPattern;
  214. names.push({ name, exploded: part.exploded });
  215. }
  216. }
  217. }
  218. pattern += '$';
  219. UriTemplate.validateLength(pattern, MAX_REGEX_LENGTH, 'Generated regex pattern');
  220. const regex = new RegExp(pattern);
  221. const match = uri.match(regex);
  222. if (!match)
  223. return null;
  224. const result = {};
  225. for (let i = 0; i < names.length; i++) {
  226. const { name, exploded } = names[i];
  227. const value = match[i + 1];
  228. const cleanName = name.replace('*', '');
  229. if (exploded && value.includes(',')) {
  230. result[cleanName] = value.split(',');
  231. }
  232. else {
  233. result[cleanName] = value;
  234. }
  235. }
  236. return result;
  237. }
  238. }
  239. //# sourceMappingURL=uriTemplate.js.map