specshield 3.4.0 → 3.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specshield",
3
- "version": "3.4.0",
3
+ "version": "3.4.2",
4
4
  "description": "Contract compatibility testing for APIs — catch breaking OpenAPI changes before they reach your consumers, with can-i-deploy deploy gating and GitHub PR checks. (a.k.a. bidirectional contract testing.)",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -330,7 +330,15 @@ const verifyCommand = new Command('verify')
330
330
  const sev = (issue.severity || 'ERROR').toUpperCase();
331
331
  const marker = sev === 'WARNING' ? chalk.yellow('⚠') : chalk.red('●');
332
332
  const type = issue.type || issue.mismatchType || 'MISMATCH';
333
- const loc = issue.field || issue.path || issue.endpoint || '$';
333
+ // Show the endpoint AND the field. One field can be read from several
334
+ // endpoints, so field alone renders genuinely distinct findings as
335
+ // identical lines that look like a duplicate-output bug — e.g. a
336
+ // `receiptUrl` removal reported once for `POST /payments` and once for
337
+ // `GET /payments/{paymentId}` both printed as "at $.receiptUrl".
338
+ const field = issue.field || issue.path || null;
339
+ const endpoint = issue.endpoint || null;
340
+ const loc = field && endpoint ? `${endpoint} ${field}`
341
+ : (field || endpoint || '$');
334
342
  process.stdout.write(` ${marker} ${chalk.bold(type)} at ${chalk.gray(loc)}\n`);
335
343
  if (issue.consumerExpects && issue.providerProvides) {
336
344
  process.stdout.write(` ${chalk.gray(`consumer: ${issue.consumerExpects}, provider: ${issue.providerProvides}`)}\n`);
@@ -92,9 +92,30 @@ const REQUIRED_FIELDS = {
92
92
  'list-consumers': ['org'],
93
93
  };
94
94
 
95
+ /**
96
+ * Option names that CAN be defaulted from `.specshield.yml`.
97
+ *
98
+ * Deliberately NOT every required field: `version`, `consumerVersion` and
99
+ * `providerVersion` are per-invocation values (a git SHA, a release tag) with no
100
+ * sensible project-wide default, so `bdctDefaultFor` has no case for them.
101
+ *
102
+ * This set exists so the "missing required options" error can tell the truth about
103
+ * where each field can be set. Pointing a user at `.specshield.yml` for a field the
104
+ * config can never supply sends them to edit a file, re-run, hit the identical error,
105
+ * and conclude the config file is broken.
106
+ *
107
+ * MUST stay in sync with the cases handled in {@link bdctDefaultFor} — the
108
+ * "config-backed fields" test asserts both directions.
109
+ */
110
+ const CONFIG_BACKED_FIELDS = new Set([
111
+ 'org', 'server', 'env',
112
+ 'provider', 'consumer', 'service',
113
+ 'spec', 'contract', 'format', 'branch',
114
+ ]);
115
+
95
116
  /**
96
117
  * Map a CLI option name to a path inside `.specshield.yml > bdct`.
97
- * Returns null when there is no defaulting rule for that option.
118
+ * Returns undefined when there is no defaulting rule for that option.
98
119
  */
99
120
  function bdctDefaultFor(bdct, name, command) {
100
121
  if (!bdct) return undefined;
@@ -181,14 +202,35 @@ function applyBdctDefaults(opts, command, { cwd = process.cwd() } = {}) {
181
202
  const missing = required.filter(k => !opts[k]);
182
203
  if (missing.length > 0) {
183
204
  const flagFor = (k) => '--' + k.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
205
+ const list = (ks) => ks.map(flagFor).join(', ');
206
+
207
+ // Split the advice: only fields the config can actually supply get pointed at
208
+ // `.specshield.yml`. The rest are flag-only and must say so, or the user edits
209
+ // the config, re-runs, and hits the same error with no idea why.
210
+ const configurable = missing.filter(k => CONFIG_BACKED_FIELDS.has(k));
211
+ const flagOnly = missing.filter(k => !CONFIG_BACKED_FIELDS.has(k));
212
+
184
213
  const msg = [
185
214
  `Missing required ${missing.length === 1 ? 'option' : 'options'} for \`bdct ${command}\`: `
186
- + missing.map(flagFor).join(', '),
215
+ + list(missing),
187
216
  ];
188
- if (cfg._file) {
189
- msg.push(`Set them as CLI flags or add them under \`bdct\` in ${cfg._file}.`);
190
- } else {
191
- msg.push('Pass them as CLI flags, or run `specshield init` to write a `.specshield.yml`.');
217
+
218
+ if (flagOnly.length > 0) {
219
+ msg.push(
220
+ ` ${list(flagOnly)} must be passed as CLI ${flagOnly.length === 1 ? 'flag' : 'flags'} ` +
221
+ `${flagOnly.length === 1 ? 'it has' : 'they have'} no \`.specshield.yml\` equivalent ` +
222
+ '(version values change per run).');
223
+ }
224
+
225
+ if (configurable.length > 0) {
226
+ msg.push(cfg._file
227
+ ? ` ${list(configurable)} can be passed as CLI flags or set under \`bdct\` in ${cfg._file}.`
228
+ : ` ${list(configurable)} can be passed as CLI flags, or run \`specshield init\` to write a \`.specshield.yml\`.`);
229
+ }
230
+
231
+ if (command === 'verify' && flagOnly.length > 0) {
232
+ msg.push(' e.g. specshield bdct verify --consumer <NAME> --provider <NAME> '
233
+ + '--consumer-version <VER> --provider-version <VER>');
192
234
  }
193
235
  const err = new Error(msg.join('\n'));
194
236
  err.code = 'MISSING_REQUIRED_OPTIONS';
@@ -205,4 +247,6 @@ module.exports = {
205
247
  clearCache,
206
248
  applyBdctDefaults,
207
249
  REQUIRED_FIELDS,
250
+ CONFIG_BACKED_FIELDS,
251
+ bdctDefaultFor,
208
252
  };