Skip to content
This repository was archived by the owner on Jan 12, 2026. It is now read-only.

Commit 07cc93a

Browse files
authored
Allow url accessible rules (#2848)
1 parent 28240ca commit 07cc93a

File tree

7 files changed

+83
-31
lines changed

7 files changed

+83
-31
lines changed

projects/optic/src/__tests__/integration/__snapshots__/diff.test.ts.snap

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,8 +533,7 @@ exports[`diff with mock server custom rules 1`] = `
533533
`;
534534

535535
exports[`diff with mock server extends 1`] = `
536-
"Extending ruleset from @test-org/ruleset-id
537-
x Empty example-api-v0.json
536+
"x Empty example-api-v0.json
538537
Operations: 1 operation added, 3 changed, 1 removed
539538
x  Checks: 3/5 passed
540539

projects/optic/src/commands/diff/generate-rule-runner.ts

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from 'path';
2-
import { ConfigRuleset, OpticCliConfig } from '../../config';
2+
import { ConfigRuleset, OpticCliConfig, initializeRules } from '../../config';
33
import { StandardRulesets } from '@useoptic/standard-rulesets';
44
import {
55
RuleRunner,
@@ -19,6 +19,16 @@ export function setRulesets(rulesets: (Ruleset | ExternalRule)[]) {
1919
}
2020

2121
const isLocalJsFile = (name: string) => name.endsWith('.js');
22+
const isUrl = (name: string) => {
23+
try {
24+
const parsed = new URL(name);
25+
if (parsed.protocol === 'http:' || parsed.protocol === 'https:')
26+
return true;
27+
return false;
28+
} catch (e) {
29+
return false;
30+
}
31+
};
2232

2333
type InputPayload = Parameters<typeof prepareRulesets>[0];
2434

@@ -35,19 +45,28 @@ const getStandardToUse = async (options: {
3545
);
3646
return config.ruleset;
3747
} else if (options.specRuleset) {
38-
try {
39-
const ruleset = await options.config.client.getStandard(
40-
options.specRuleset
41-
);
42-
return ruleset.config.ruleset;
43-
} catch (e) {
44-
logger.warn(
45-
`${chalk.red('Warning:')} Could not download standard ${
48+
if (options.specRuleset.startsWith('@')) {
49+
try {
50+
const ruleset = await options.config.client.getStandard(
4651
options.specRuleset
47-
}. Please check the ruleset name and whether you are authenticated (run: optic login).`
48-
);
49-
process.exitCode = 1;
50-
return [];
52+
);
53+
return ruleset.config.ruleset;
54+
} catch (e) {
55+
logger.warn(
56+
`${chalk.red('Warning:')} Could not download standard ${
57+
options.specRuleset
58+
}. Please check the ruleset name and whether you are authenticated (run: optic login).`
59+
);
60+
process.exitCode = 1;
61+
return [];
62+
}
63+
} else {
64+
const rules: any = {
65+
extends: options.specRuleset,
66+
};
67+
await initializeRules(rules, options.config.client);
68+
69+
return rules.ruleset;
5170
}
5271
} else {
5372
return options.config.ruleset;
@@ -105,6 +124,12 @@ export const generateRuleRunner = async (
105124
? path.dirname(options.config.configPath)
106125
: process.cwd();
107126
localRulesets[rule.name] = path.resolve(rootPath, rule.name); // the path is the name
127+
} else if (isUrl(rule.name)) {
128+
hostedRulesets[rule.name] = {
129+
uploaded_at: String(Math.random()),
130+
url: rule.name,
131+
should_decompress: false,
132+
};
108133
} else {
109134
rulesToFetch.push(rule.name);
110135
}
@@ -118,6 +143,7 @@ export const generateRuleRunner = async (
118143
hostedRulesets[hostedRuleset.name] = {
119144
uploaded_at: hostedRuleset.uploaded_at,
120145
url: hostedRuleset.url,
146+
should_decompress: true,
121147
};
122148
}
123149
}

projects/optic/src/config.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import fs from 'node:fs/promises';
2+
import fetch from 'node-fetch';
23
import yaml from 'js-yaml';
34
import { UserError, isTruthyStringValue } from '@useoptic/openapi-utilities';
45
import Ajv from 'ajv';
@@ -217,14 +218,28 @@ export const initializeRules = async (
217218
client: OpticBackendClient
218219
) => {
219220
let rulesetMap: Map<string, ConfigRuleset> = new Map();
221+
let rawRulesets = config.ruleset ? config.ruleset : [];
220222
if (config.extends) {
221-
console.log(`Extending ruleset from ${config.extends}`);
223+
logger.debug(`Extending ruleset from ${config.extends}`);
222224

223225
try {
224-
const response = await client.getStandard(config.extends);
225-
rulesetMap = new Map(
226-
response.config.ruleset.map((conf) => [conf.name, conf])
227-
);
226+
if (config.extends.startsWith('@')) {
227+
const response = await client.getStandard(config.extends);
228+
rulesetMap = new Map(
229+
response.config.ruleset.map((conf) => [conf.name, conf])
230+
);
231+
} else {
232+
// Assumption is that we're fetching a yaml file
233+
const response = await fetch(config.extends).then((response) => {
234+
if (response.status !== 200) {
235+
throw new Error(`received status code ${response.status}`);
236+
} else {
237+
return response.text();
238+
}
239+
});
240+
const parsed = yaml.load(response);
241+
rawRulesets.push(...(parsed as any).ruleset);
242+
}
228243
} catch (e) {
229244
console.error(e);
230245
console.log(
@@ -233,8 +248,8 @@ export const initializeRules = async (
233248
}
234249
}
235250

236-
if (config.ruleset) {
237-
for (const ruleset of config.ruleset) {
251+
if (rawRulesets.length) {
252+
for (const ruleset of rawRulesets) {
238253
if (typeof ruleset === 'string') {
239254
rulesetMap.set(ruleset, { name: ruleset, config: {} });
240255
} else if (typeof ruleset === 'object' && ruleset !== null) {

projects/rulesets-base/src/custom-rulesets/__tests__/download-ruleset.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ describe('downloadRuleset', () => {
2020
downloadRuleset(
2121
'test-ruleset',
2222
'https://some-url.com',
23-
'2022-11-01T19:32:22.148Z'
23+
'2022-11-01T19:32:22.148Z',
24+
true
2425
)
2526
).rejects.toThrow(new Error('Downloading ruleset failed (404): Missing'));
2627
});
@@ -35,7 +36,8 @@ describe('downloadRuleset', () => {
3536
await downloadRuleset(
3637
'test-ruleset',
3738
'https://some-url.com',
38-
'2022-11-01T19:32:22.148Z'
39+
'2022-11-01T19:32:22.148Z',
40+
true
3941
);
4042
expect(fs.mkdir).toBeCalled();
4143
expect(fs.writeFile).toHaveBeenCalledWith(
@@ -49,7 +51,8 @@ describe('downloadRuleset', () => {
4951
await downloadRuleset(
5052
'test-ruleset',
5153
'https://some-url.com',
52-
'2022-11-01T19:32:22.148Z'
54+
'2022-11-01T19:32:22.148Z',
55+
true
5356
);
5457

5558
expect(fetch).not.toBeCalled();

projects/rulesets-base/src/custom-rulesets/__tests__/prepare-ruleset.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe('prepareRulesets', () => {
3232
'@team/custom-ruleset': {
3333
url: 'https://some-url.com',
3434
uploaded_at: '123',
35+
should_decompress: true,
3536
},
3637
},
3738
standardRulesets: {
@@ -76,6 +77,7 @@ describe('prepareRulesets', () => {
7677
'@team/custom-ruleset': {
7778
url: 'https://some-url.com',
7879
uploaded_at: '123',
80+
should_decompress: true,
7981
},
8082
},
8183
standardRulesets: {

projects/rulesets-base/src/custom-rulesets/download-ruleset.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import path from 'node:path';
77
export async function downloadRuleset(
88
name: string,
99
url: string,
10-
uploaded_at: string
10+
uploaded_at: string,
11+
should_decompress: boolean
1112
): Promise<string> {
1213
const filepath = path.join(os.tmpdir(), name, `${uploaded_at}.js`);
1314
try {
@@ -22,14 +23,18 @@ export async function downloadRuleset(
2223
`Downloading ruleset failed (${resp.status}): ${await resp.text()}`
2324
);
2425
}
25-
26-
const compressed = await resp.buffer();
27-
const decompressed = zlib.brotliDecompressSync(compressed);
26+
let raw: Buffer;
27+
if (should_decompress) {
28+
const compressed = await resp.buffer();
29+
raw = zlib.brotliDecompressSync(compressed);
30+
} else {
31+
raw = await resp.buffer();
32+
}
2833

2934
const filefolder = path.dirname(filepath);
3035
// Does not error if folder exists when recursive = true
3136
await fs.mkdir(filefolder, { recursive: true });
32-
await fs.writeFile(filepath, decompressed);
37+
await fs.writeFile(filepath, raw);
3338

3439
return filepath;
3540
}

projects/rulesets-base/src/custom-rulesets/prepare-rulesets.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type RulesetPayload = {
1515
{
1616
uploaded_at: string;
1717
url: string;
18+
should_decompress: boolean;
1819
}
1920
>;
2021
standardRulesets: Record<
@@ -81,7 +82,8 @@ export async function prepareRulesets(
8182
rulesetPath = await downloadRuleset(
8283
ruleset.name,
8384
hostedRuleset.url,
84-
hostedRuleset.uploaded_at
85+
hostedRuleset.uploaded_at,
86+
hostedRuleset.should_decompress
8587
);
8688
} catch (e) {
8789
warnings.push(`Loading ruleset ${ruleset.name} failed`);

0 commit comments

Comments
 (0)