Repository: Redocly/redocly-cli
Pull request #809 (2022-08-16 14:13:06)

feat: create config dynamically
## What/Why/How?

In case we are running `openapi-core` in environments not having access to file system we have to be able to create config from string or object.

## Reference

## Testing

## Screenshots (optional)

## Check yourself

- [ ] Code is linted
- [ ] Tested with redoc/reference-docs/workflows
- [ ] All new/updated code is covered with tests

## Security

- [ ] Security impact of change has been considered
- [ ] Code follows company security practices and guidelines


github-actions[bot]: <!-- jest coverage report action for options with hash 3339ae53a38c44382c0fb4491bdb282d -->

## Coverage report 

❌ An unexpected error occurred. For more details, [check console](https://github.com/Redocly/redocly-cli/actions/runs/2868635980) 
```
Error: The process '/usr/local/bin/npm' failed with exit code 1
```

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | Category   | Percentage | Covered / Total |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :--------- | :--------- | :-------------: |
|                                                                           🟡                                                                           | Statements | 68.57%     |    2884/4206    |
|                                                                           🟡                                                                           | Branches   | 61.87%     |    1592/2573    |
|                                                                           🟡                                                                           | Functions  | 60%        |     462/770     |
|                                                                           🟡                                                                           | Lines      | 68.37%     |    2672/3908    |


<details><summary>Show files with reduced coverage 🔻</summary>
<br/>

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | File                                                       | Statements                                                     | Branches                                                       | Functions                                                     | Lines                                                         |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------- | :------------------------------------------------------------- | :------------------------------------------------------------- | :------------------------------------------------------------ | :------------------------------------------------------------ |
|                                                                           🟢                                                                           | <div title="core/src/config/load.ts">`...` / load.ts</div> | <div title="Base coverage is: 90.91%">85.71% (-5.19% 🔻)</div> | <div title="Base coverage is: 76.92%">71.88% (-5.05% 🔻)</div> | <div title="Base coverage is: 100%">83.33% (-16.67% 🔻)</div> | <div title="Base coverage is: 92.5%">86.67% (-5.83% 🔻)</div> |
|                                                                           🟢                                                                           | core/src/index.ts                                          | 100%                                                           | 100%                                                           | 8.16%                                                         | 100%                                                          |

</details>

## Test suite run failed
Failed tests: 0/460. Failed suites: 0/77.

<p align="right">Report generated by <a href="https://github.com/ArtiomTr/jest-coverage-report-action">🧪jest coverage report action</a> from 8dae0c31f3ac8e3c847aa2e72d7ca49f0183df0b</p>


Patch:
diff --git a/packages/core/src/config/load.ts b/packages/core/src/config/load.ts
index 19a1a86e..582a1b2e 100644
--- a/packages/core/src/config/load.ts
+++ b/packages/core/src/config/load.ts
@@ -2,20 +2,24 @@ import * as fs from 'fs';
 import * as path from 'path';
 import { RedoclyClient } from '../redocly';
 import { isEmptyObject, loadYaml, doesYamlFileExist } from '../utils';
+import { parseYaml } from '../js-yaml';
 import { Config, DOMAINS } from './config';
 import { transformConfig } from './utils';
 import { resolveConfig } from './config-resolvers';
 
 import type { DeprecatedInRawConfig, RawConfig, Region } from './types';
+import { RegionalTokenWithValidity } from '../redocly/redocly-client-types';
 
 async function addConfigMetadata({
   rawConfig,
   customExtends,
   configPath,
+  tokens,
 }: {
   rawConfig: RawConfig;
   customExtends?: string[];
   configPath?: string;
+  tokens?: RegionalTokenWithValidity[];
 }): Promise<Config> {
   if (customExtends !== undefined) {
     rawConfig.styleguide = rawConfig.styleguide || {};
@@ -25,10 +29,7 @@ async function addConfigMetadata({
     // rawConfig.styleguide = { extends: ['recommended'], recommendedFallback: true };
   }
 
-  const redoclyClient = new RedoclyClient();
-  const tokens = await redoclyClient.getTokens();
-
-  if (tokens.length) {
+  if (tokens?.length) {
     if (!rawConfig.resolve) rawConfig.resolve = {};
     if (!rawConfig.resolve.http) rawConfig.resolve.http = {};
     rawConfig.resolve.http.headers = [...(rawConfig.resolve.http.headers ?? [])];
@@ -69,10 +70,15 @@ export async function loadConfig(
   if (typeof processRawConfig === 'function') {
     await processRawConfig(rawConfig);
   }
-  return await addConfigMetadata({
+
+  const redoclyClient = new RedoclyClient();
+  const tokens = await redoclyClient.getTokens();
+
+  return addConfigMetadata({
     rawConfig,
     customExtends,
     configPath,
+    tokens,
   });
 }
 
@@ -102,3 +108,20 @@ export async function getConfig(configPath: string | undefined = findConfig()):
     throw new Error(`Error parsing config file at '${configPath}': ${e.message}`);
   }
 }
+
+type CreateConfigOptions = {
+  extends?: string[];
+  tokens?: RegionalTokenWithValidity[];
+};
+
+export async function createConfig(
+  config: string | RawConfig,
+  options?: CreateConfigOptions
+): Promise<Config> {
+  return addConfigMetadata({
+    rawConfig: transformConfig(
+      typeof config === 'string' ? (parseYaml(config) as RawConfig) : config
+    ),
+    ...options,
+  });
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 72382b59..046d63ce 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -35,6 +35,7 @@ export {
   findConfig,
   CONFIG_FILE_NAMES,
   RuleSeverity,
+  createConfig,
 } from './config';
 
 export { RedoclyClient, isRedoclyRegistryURL } from './redocly';


Test patch:
diff --git a/packages/core/src/config/__tests__/load.test.ts b/packages/core/src/config/__tests__/load.test.ts
index dc349758..66bce5f8 100644
--- a/packages/core/src/config/__tests__/load.test.ts
+++ b/packages/core/src/config/__tests__/load.test.ts
@@ -1,5 +1,7 @@
-import { loadConfig, findConfig, getConfig } from '../load';
+import { loadConfig, findConfig, getConfig, createConfig } from '../load';
 import { RedoclyClient } from '../../redocly';
+import { RuleConfig, RawConfig } from './../types';
+import { Config } from '../config';
 
 const fs = require('fs');
 const path = require('path');
@@ -87,3 +89,79 @@ describe('getConfig', () => {
     expect(getConfig()).toEqual(Promise.resolve({}));
   });
 });
+
+describe('createConfig', () => {
+  it('should create config from string', async () => {
+    const config = await createConfig(`
+      styleguide:
+        extends:
+          - recommended
+        rules:
+          info-license: off
+    `);
+
+    verifyExtendedConfig(config, {
+      extendsRuleSet: 'recommended',
+      overridesRules: { 'info-license': 'off' },
+    });
+  });
+
+  it('should create config from object', async () => {
+    const rawConfig: RawConfig = {
+      styleguide: {
+        extends: ['minimal'],
+        rules: {
+          'info-license': 'off',
+          'tag-description': 'off',
+          'operation-2xx-response': 'off',
+        },
+      },
+    };
+    const config = await createConfig(rawConfig);
+
+    verifyExtendedConfig(config, {
+      extendsRuleSet: 'minimal',
+      overridesRules: rawConfig.styleguide!.rules as Record<string, RuleConfig>,
+    });
+  });
+});
+
+function verifyExtendedConfig(
+  config: Config,
+  {
+    extendsRuleSet,
+    overridesRules,
+  }: { extendsRuleSet: string; overridesRules: Record<string, RuleConfig> }
+) {
+  const defaultPlugin = config.styleguide.plugins.find((plugin) => plugin.id === '');
+  expect(defaultPlugin).toBeDefined();
+
+  const recommendedRules = defaultPlugin?.configs?.[extendsRuleSet];
+  expect(recommendedRules).toBeDefined();
+
+  verifyOasRules(config.styleguide.rules.oas2, overridesRules, recommendedRules?.rules || {});
+  verifyOasRules(
+    config.styleguide.rules.oas3_0,
+    overridesRules,
+    Object.assign({}, recommendedRules?.rules, recommendedRules?.oas3_0Rules)
+  );
+  verifyOasRules(
+    config.styleguide.rules.oas3_1,
+    overridesRules,
+    Object.assign({}, recommendedRules?.rules, recommendedRules?.oas3_1Rules)
+  );
+}
+
+function verifyOasRules(
+  finalRuleset: Record<string, RuleConfig>,
+  overridesRules: Record<string, RuleConfig>,
+  defaultRuleset: Record<string, RuleConfig>
+) {
+  Object.entries(finalRuleset).forEach(([ruleName, ruleValue]) => {
+    if (ruleName in overridesRules) {
+      expect(ruleValue).toBe(overridesRules[ruleName]);
+    } else {
+      expect(ruleValue).toBe(defaultRuleset[ruleName]);
+    }
+  });
+}