Repository: stoplightio/spectral
Pull request #220 (2019-05-21 11:37:57)

feat: change string interpolation implementation
### Please check if the PR fulfills these requirements
- [x] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)

### What kind of change does this PR introduce?

Different string interpolation.

### What is the current behavior?

The current implementation is fine, but won't work once we convert our rulesets to YAML/JSON.

### If this is a feature change, what is the new behavior?

### Does this PR introduce a breaking change?

There is no breaking change. Actual messages are untouched.

What changes might users need to make in their application due to this PR?

### Other information

(e.g. detailed explanation, related issues, links for us to have context, eg. stackoverflow, issues outside of the repo, forum, etc.)



Patch:
diff --git a/src/linter.ts b/src/linter.ts
index db65edef..93f2418e 100644
--- a/src/linter.ts
+++ b/src/linter.ts
@@ -2,6 +2,7 @@ import { DiagnosticSeverity, JsonPath } from '@stoplight/types';
 import * as jp from 'jsonpath';
 import { get, has } from 'lodash';
 
+import { message } from './rulesets/message';
 import { IFunction, IGivenNode, IParsedResult, IRuleResult, IRunOpts, IRunRule, IThen } from './types';
 
 // TODO(SO-23): unit test but mock whatShouldBeLinted
@@ -102,13 +103,11 @@ export const lintNode = (
           message:
             rule.message === undefined
               ? rule.summary || result.message
-              : typeof rule.message === 'function'
-                ? rule.message({
-                    error: result.message,
-                    property: path.length > 0 ? path[path.length - 1] : '',
-                    description: rule.description,
-                  })
-                : rule.message,
+              : message(rule.message, {
+                  error: result.message,
+                  property: path.length > 0 ? path[path.length - 1] : '',
+                  description: rule.description,
+                }),
           path,
           severity,
           source: parsedResult.source,
diff --git a/src/rulesets/message.ts b/src/rulesets/message.ts
index e173561f..e3c4f5a4 100644
--- a/src/rulesets/message.ts
+++ b/src/rulesets/message.ts
@@ -4,16 +4,20 @@ export interface IMessageVars {
   description?: string;
 }
 
-export type MessageInterpolator = (values: IMessageVars) => string;
+export type MessageInterpolator = (str: string, values: IMessageVars) => string;
 
-export function message(strings: TemplateStringsArray, ...vars: Array<keyof IMessageVars>) {
-  return (values: IMessageVars) => {
-    const result = [strings[0]];
+const BRACES = /{{([^}]+)}}/g;
 
-    for (const [i, key] of vars.entries()) {
-      result.push(String(values[key] || ''), strings[i + 1]);
-    }
+export const message: MessageInterpolator = (str, values) => {
+  BRACES.lastIndex = 0;
+  let result: RegExpExecArray | null = null;
 
-    return result.join('');
-  };
-}
+  // tslint:disable-next-line:no-conditional-assignment
+  while ((result = BRACES.exec(str))) {
+    const newValue = String(values[result[1]] || '');
+    str = `${str.slice(0, result.index)}${newValue}${str.slice(BRACES.lastIndex)}`;
+    BRACES.lastIndex = result.index + newValue.length;
+  }
+
+  return str;
+};
diff --git a/src/rulesets/oas/index.ts b/src/rulesets/oas/index.ts
index 35cce1b6..df7b2efa 100644
--- a/src/rulesets/oas/index.ts
+++ b/src/rulesets/oas/index.ts
@@ -1,6 +1,5 @@
 import { DiagnosticSeverity } from '@stoplight/types';
 import { FunctionCollection, RuleCollection, RuleFunction, RuleType } from '../../types';
-import { message } from '../message';
 
 export const operationPath =
   "$..paths.*[?( name() === 'get' || name() === 'put' || name() === 'post'" +
@@ -63,7 +62,7 @@ export const commonOasRules = (): RuleCollection => ({
   },
   'path-params': {
     summary: 'Path parameters are correct and valid.',
-    message: message`${'error'}`,
+    message: '{{error}}',
     type: RuleType.VALIDATION,
     severity: DiagnosticSeverity.Error,
     given: '$',
diff --git a/src/rulesets/oas2/index.ts b/src/rulesets/oas2/index.ts
index 24d92c94..d3edd31b 100644
--- a/src/rulesets/oas2/index.ts
+++ b/src/rulesets/oas2/index.ts
@@ -2,7 +2,6 @@ const merge = require('lodash/merge');
 
 import { DiagnosticSeverity } from '@stoplight/types';
 import { RuleFunction, RuleType } from '../../types';
-import { message } from '../message';
 import { commonOasRules } from '../oas';
 import * as schema from './schemas/main.json';
 
@@ -12,7 +11,7 @@ export const oas2Rules = () => {
   return merge(commonOasRules(), {
     // specification validation
     'oas2-schema': {
-      message: message`${'error'}`,
+      message: '{{error}}',
       type: RuleType.VALIDATION,
       severity: DiagnosticSeverity.Error,
       then: {
@@ -102,7 +101,7 @@ export const oas2Rules = () => {
       tags: ['operation'],
     },
     'valid-example': {
-      message: message`"${'property'}" property ${'error'}`,
+      message: '"{{property}}" property {{error}}',
       type: RuleType.VALIDATION,
       given: '$..[?(@.example)]',
       then: {
diff --git a/src/rulesets/oas3/index.ts b/src/rulesets/oas3/index.ts
index 43584b56..397a1c0c 100644
--- a/src/rulesets/oas3/index.ts
+++ b/src/rulesets/oas3/index.ts
@@ -2,7 +2,6 @@ const merge = require('lodash/merge');
 
 import { DiagnosticSeverity } from '@stoplight/types';
 import { RuleFunction, RuleType } from '../../types';
-import { message } from '../message';
 import { commonOasRules } from '../oas';
 import * as schema from './schemas/main.json';
 
@@ -12,7 +11,7 @@ export const oas3Rules = () => {
   return merge(commonOasRules(), {
     // specification validation
     'oas3-schema': {
-      message: message`${'error'}`,
+      message: '{{error}}',
       type: RuleType.VALIDATION,
       severity: DiagnosticSeverity.Error,
       then: {
@@ -92,7 +91,7 @@ export const oas3Rules = () => {
       },
     },
     'valid-example': {
-      message: message`"${'property'}" property ${'error'}`,
+      message: '"{{property}}" property {{error}}',
       type: RuleType.VALIDATION,
       given: '$..[?(@.example)]',
       then: {
diff --git a/src/types/rule.ts b/src/types/rule.ts
index 6e3a7d0f..d031c019 100644
--- a/src/types/rule.ts
+++ b/src/types/rule.ts
@@ -1,5 +1,4 @@
 import { DiagnosticSeverity } from '@stoplight/types';
-import { MessageInterpolator } from '../rulesets/message';
 import { RuleFunction, RuleType } from './enums';
 
 export type Rule = IRule | TruthyRule | XorRule | LengthRule | AlphaRule | PatternRule | SchemaRule;
@@ -11,7 +10,7 @@ export interface IRule<T = string, O = any> {
   summary?: string;
 
   // A meaningful feedback about the error
-  message?: string | MessageInterpolator;
+  message?: string;
 
   // A long-form description of the rule formatted in markdown
   description?: string;


Test patch:
diff --git a/src/rulesets/oas/__tests__/message.spec.ts b/src/rulesets/oas/__tests__/message.spec.ts
index cb37f991..352f7e75 100644
--- a/src/rulesets/oas/__tests__/message.spec.ts
+++ b/src/rulesets/oas/__tests__/message.spec.ts
@@ -2,12 +2,32 @@ import { message } from '../../message';
 
 describe('message util', () => {
   test('interpolates correctly', () => {
-    const val = message`oops... ${'property'} missing;error: ${'error'}`;
+    const template = 'oops... "{{property}}" is missing;error: {{error}}';
     expect(
-      val({
+      message(template, {
         property: 'description',
         error: 'expected property to be truthy',
       }),
-    ).toEqual('oops... description missing;error: expected property to be truthy');
+    ).toEqual('oops... "description" is missing;error: expected property to be truthy');
+  });
+
+  test('handles siblings', () => {
+    const template = '{{error}}{{error}}{{property}}{{bar}}{{error}}{{error}}';
+    expect(
+      message(template, {
+        property: 'baz',
+        error: 'foo',
+      }),
+    ).toEqual('foofoobazfoofoo');
+  });
+
+  test('strips missing keys', () => {
+    const template = '{{foo}}missing {{bar}}:(';
+    expect(
+      message(template, {
+        property: 'description',
+        error: 'expected property to be truthy',
+      }),
+    ).toEqual('missing :(');
   });
 });