refactor: unify how validateOptions is handled (#6961)

* refactor: unify how validateOptions is handled

* fix types

* fix again
This commit is contained in:
Joshua Chen 2022-03-22 19:40:56 +08:00 committed by GitHub
parent 44107fb879
commit 6e2eb44964
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 542 additions and 540 deletions

View file

@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`validateOptions throws Error in case of invalid feed type 1`] = `"\\"feedOptions.type\\" does not match any of the allowed types"`;
exports[`validateOptions throws Error in case of invalid options 1`] = `"\\"postsPerPage\\" must be greater than or equal to 1"`;

View file

@ -1,5 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`blog plugin options schema throws Error in case of invalid feed type 1`] = `[ValidationError: "feedOptions.type" does not match any of the allowed types]`;
exports[`blog plugin options schema throws Error in case of invalid options 1`] = `[ValidationError: "postsPerPage" must be greater than or equal to 1]`;

View file

@ -11,7 +11,7 @@ import fs from 'fs-extra';
import {createBlogFeedFiles} from '../feed';
import type {LoadContext, I18n} from '@docusaurus/types';
import type {BlogContentPaths} from '../types';
import {DEFAULT_OPTIONS} from '../pluginOptionSchema';
import {DEFAULT_OPTIONS} from '../options';
import {generateBlogPosts} from '../blogUtils';
import type {PluginOptions} from '@docusaurus/plugin-content-blog';

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {validateBlogPostFrontMatter} from '../blogFrontMatter';
import {validateBlogPostFrontMatter} from '../frontMatter';
import escapeStringRegexp from 'escape-string-regexp';
import type {BlogPostFrontMatter} from '@docusaurus/plugin-content-blog';

View file

@ -9,9 +9,9 @@ import {jest} from '@jest/globals';
import path from 'path';
import pluginContentBlog from '../index';
import type {DocusaurusConfig, LoadContext, I18n} from '@docusaurus/types';
import {PluginOptionSchema} from '../pluginOptionSchema';
import {validateOptions} from '../options';
import type {BlogPost} from '../types';
import type {Joi} from '@docusaurus/utils-validation';
import {normalizePluginOptions} from '@docusaurus/utils-validation';
import {posixPath, getFileCommitDate} from '@docusaurus/utils';
import type {
PluginOptions,
@ -47,18 +47,6 @@ function getI18n(locale: string): I18n {
const DefaultI18N: I18n = getI18n('en');
function validateAndNormalize(
schema: Joi.ObjectSchema,
options: Partial<PluginOptions>,
) {
const {value, error} = schema.validate(options);
if (error) {
throw error;
} else {
return value;
}
}
const PluginPath = 'blog';
const BaseEditUrl = 'https://baseEditUrl.com/edit';
@ -81,11 +69,14 @@ const getPlugin = async (
generatedFilesDir,
i18n,
} as LoadContext,
validateAndNormalize(PluginOptionSchema, {
path: PluginPath,
editUrl: BaseEditUrl,
...pluginOptions,
}),
validateOptions({
validate: normalizePluginOptions,
options: {
path: PluginPath,
editUrl: BaseEditUrl,
...pluginOptions,
},
}) as PluginOptions,
);
};

View file

@ -0,0 +1,160 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {validateOptions, DEFAULT_OPTIONS} from '../options';
import {normalizePluginOptions} from '@docusaurus/utils-validation';
import type {Options} from '@docusaurus/plugin-content-blog';
function testValidate(options: Options) {
return validateOptions({validate: normalizePluginOptions, options});
}
// the type of remark/rehype plugins can be either function, object or array
const markdownPluginsFunctionStub = () => {};
const markdownPluginsObjectStub = {};
const defaultOptions = {...DEFAULT_OPTIONS, id: 'default'};
describe('validateOptions', () => {
it('returns default options for undefined user options', () => {
expect(testValidate(undefined)).toEqual(defaultOptions);
});
it('returns default options for empty user options', () => {
expect(testValidate({})).toEqual(defaultOptions);
});
it('accepts correctly defined user options', () => {
const userOptions = {
...defaultOptions,
feedOptions: {type: 'rss' as const, title: 'myTitle'},
path: 'not_blog',
routeBasePath: 'myBlog',
postsPerPage: 5,
include: ['api/*', 'docs/*'],
};
expect(testValidate(userOptions)).toEqual({
...userOptions,
feedOptions: {type: ['rss'], title: 'myTitle', copyright: ''},
});
});
it('accepts valid user options', () => {
const userOptions = {
...defaultOptions,
routeBasePath: 'myBlog',
beforeDefaultRemarkPlugins: [],
beforeDefaultRehypePlugins: [markdownPluginsFunctionStub],
remarkPlugins: [[markdownPluginsFunctionStub, {option1: '42'}]],
rehypePlugins: [
markdownPluginsObjectStub,
[markdownPluginsFunctionStub, {option1: '42'}],
],
};
expect(testValidate(userOptions)).toEqual(userOptions);
});
it('throws Error in case of invalid options', () => {
expect(() =>
testValidate({
path: 'not_blog',
postsPerPage: -1,
include: ['api/*', 'docs/*'],
routeBasePath: 'not_blog',
}),
).toThrowErrorMatchingSnapshot();
});
it('throws Error in case of invalid feed type', () => {
expect(() =>
testValidate({
feedOptions: {
type: 'none',
},
}),
).toThrowErrorMatchingSnapshot();
});
it('converts all feed type to array with other feed type', () => {
expect(
testValidate({
feedOptions: {type: 'all'},
}),
).toEqual({
...defaultOptions,
feedOptions: {type: ['rss', 'atom', 'json'], copyright: ''},
});
});
it('accepts null type and return same', () => {
expect(
testValidate({
feedOptions: {type: null},
}),
).toEqual({
...defaultOptions,
feedOptions: {type: null},
});
});
it('contains array with rss + atom for missing feed type', () => {
expect(
testValidate({
feedOptions: {},
}),
).toEqual(defaultOptions);
});
it('has array with rss + atom, title for missing feed type', () => {
expect(
testValidate({
feedOptions: {title: 'title'},
}),
).toEqual({
...defaultOptions,
feedOptions: {type: ['rss', 'atom'], title: 'title', copyright: ''},
});
});
it('accepts 0 sidebar count', () => {
const userOptions = {blogSidebarCount: 0};
expect(testValidate(userOptions)).toEqual({
...defaultOptions,
...userOptions,
});
});
it('accepts "ALL" sidebar count', () => {
const userOptions = {blogSidebarCount: 'ALL' as const};
expect(testValidate(userOptions)).toEqual({
...defaultOptions,
...userOptions,
});
});
it('rejects "abcdef" sidebar count', () => {
const userOptions = {blogSidebarCount: 'abcdef'};
expect(() => testValidate(userOptions)).toThrowErrorMatchingInlineSnapshot(
`"\\"blogSidebarCount\\" must be one of [ALL, number]"`,
);
});
it('accepts "all posts" sidebar title', () => {
const userOptions = {blogSidebarTitle: 'all posts'};
expect(testValidate(userOptions)).toEqual({
...defaultOptions,
...userOptions,
});
});
it('rejects 42 sidebar title', () => {
const userOptions = {blogSidebarTitle: 42};
expect(() => testValidate(userOptions)).toThrowErrorMatchingInlineSnapshot(
`"\\"blogSidebarTitle\\" must be a string"`,
);
});
});

View file

@ -1,152 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {PluginOptionSchema, DEFAULT_OPTIONS} from '../pluginOptionSchema';
// the type of remark/rehype plugins can be either function, object or array
const markdownPluginsFunctionStub = () => {};
const markdownPluginsObjectStub = {};
describe('blog plugin options schema', () => {
it('normalizes options', () => {
const {value, error} = PluginOptionSchema.validate({});
expect(value).toEqual(DEFAULT_OPTIONS);
expect(error).toBeUndefined();
});
it('accepts correctly defined user options', () => {
const userOptions = {
...DEFAULT_OPTIONS,
feedOptions: {type: 'rss', title: 'myTitle'},
path: 'not_blog',
routeBasePath: 'myBlog',
postsPerPage: 5,
include: ['api/*', 'docs/*'],
};
const {value, error} = PluginOptionSchema.validate(userOptions);
expect(value).toEqual({
...userOptions,
feedOptions: {type: ['rss'], title: 'myTitle', copyright: ''},
});
expect(error).toBeUndefined();
});
it('accepts valid user options', async () => {
const userOptions = {
...DEFAULT_OPTIONS,
routeBasePath: 'myBlog',
beforeDefaultRemarkPlugins: [],
beforeDefaultRehypePlugins: [markdownPluginsFunctionStub],
remarkPlugins: [[markdownPluginsFunctionStub, {option1: '42'}]],
rehypePlugins: [
markdownPluginsObjectStub,
[markdownPluginsFunctionStub, {option1: '42'}],
],
};
const {value, error} = PluginOptionSchema.validate(userOptions);
expect(value).toEqual(userOptions);
expect(error).toBeUndefined();
});
it('throws Error in case of invalid options', () => {
const {error} = PluginOptionSchema.validate({
path: 'not_blog',
postsPerPage: -1,
include: ['api/*', 'docs/*'],
routeBasePath: 'not_blog',
});
expect(error).toMatchSnapshot();
});
it('throws Error in case of invalid feed type', () => {
const {error} = PluginOptionSchema.validate({
feedOptions: {
type: 'none',
},
});
expect(error).toMatchSnapshot();
});
it('converts all feed type to array with other feed type', () => {
const {value} = PluginOptionSchema.validate({
feedOptions: {type: 'all'},
});
expect(value).toEqual({
...DEFAULT_OPTIONS,
feedOptions: {type: ['rss', 'atom', 'json'], copyright: ''},
});
});
it('accepts null type and return same', () => {
const {value, error} = PluginOptionSchema.validate({
feedOptions: {type: null},
});
expect(value).toEqual({
...DEFAULT_OPTIONS,
feedOptions: {type: null},
});
expect(error).toBeUndefined();
});
it('contains array with rss + atom for missing feed type', () => {
const {value} = PluginOptionSchema.validate({
feedOptions: {},
});
expect(value).toEqual(DEFAULT_OPTIONS);
});
it('has array with rss + atom, title for missing feed type', () => {
const {value} = PluginOptionSchema.validate({
feedOptions: {title: 'title'},
});
expect(value).toEqual({
...DEFAULT_OPTIONS,
feedOptions: {type: ['rss', 'atom'], title: 'title', copyright: ''},
});
});
});
describe('blog sidebar', () => {
it('accepts 0 sidebar count', () => {
const userOptions = {blogSidebarCount: 0};
const {value, error} = PluginOptionSchema.validate(userOptions);
expect(value).toEqual({...DEFAULT_OPTIONS, ...userOptions});
expect(error).toBeUndefined();
});
it('accepts "ALL" sidebar count', () => {
const userOptions = {blogSidebarCount: 'ALL'};
const {value, error} = PluginOptionSchema.validate(userOptions);
expect(value).toEqual({...DEFAULT_OPTIONS, ...userOptions});
expect(error).toBeUndefined();
});
it('rejects "abcdef" sidebar count', () => {
const userOptions = {blogSidebarCount: 'abcdef'};
const {error} = PluginOptionSchema.validate(userOptions);
expect(error).toMatchInlineSnapshot(
`[ValidationError: "blogSidebarCount" must be one of [ALL, number]]`,
);
});
it('accepts "all posts" sidebar title', () => {
const userOptions = {blogSidebarTitle: 'all posts'};
const {value, error} = PluginOptionSchema.validate(userOptions);
expect(value).toEqual({...DEFAULT_OPTIONS, ...userOptions});
expect(error).toBeUndefined();
});
it('rejects 42 sidebar title', () => {
const userOptions = {blogSidebarTitle: 42};
const {error} = PluginOptionSchema.validate(userOptions);
expect(error).toMatchInlineSnapshot(
`[ValidationError: "blogSidebarTitle" must be a string]`,
);
});
});

View file

@ -7,7 +7,7 @@
import type {BlogPost, BlogContent} from '../types';
import {getTranslationFiles, translateContent} from '../translations';
import {DEFAULT_OPTIONS} from '../pluginOptionSchema';
import {DEFAULT_OPTIONS} from '../options';
import {updateTranslationFileMessages} from '@docusaurus/utils';
import type {PluginOptions} from '@docusaurus/plugin-content-blog';

View file

@ -31,7 +31,7 @@ import {
getContentPathList,
} from '@docusaurus/utils';
import type {LoadContext} from '@docusaurus/types';
import {validateBlogPostFrontMatter} from './blogFrontMatter';
import {validateBlogPostFrontMatter} from './frontMatter';
import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
import logger from '@docusaurus/logger';
import type {

View file

@ -30,14 +30,7 @@ import type {
BlogContentPaths,
BlogMarkdownLoaderOptions,
} from './types';
import {PluginOptionSchema} from './pluginOptionSchema';
import type {
LoadContext,
Plugin,
HtmlTags,
OptionValidationContext,
ValidationResult,
} from '@docusaurus/types';
import type {LoadContext, Plugin, HtmlTags} from '@docusaurus/types';
import {
generateBlogPosts,
getSourceToPermalink,
@ -572,10 +565,4 @@ export default async function pluginContentBlog(
};
}
export function validateOptions({
validate,
options,
}: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> {
const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions;
}
export {validateOptions} from './options';

View file

@ -13,7 +13,8 @@ import {
URISchema,
} from '@docusaurus/utils-validation';
import {GlobExcludeDefault} from '@docusaurus/utils';
import type {PluginOptions} from '@docusaurus/plugin-content-blog';
import type {PluginOptions, Options} from '@docusaurus/plugin-content-blog';
import type {OptionValidationContext} from '@docusaurus/types';
export const DEFAULT_OPTIONS: PluginOptions = {
feedOptions: {type: ['rss', 'atom'], copyright: ''},
@ -46,7 +47,7 @@ export const DEFAULT_OPTIONS: PluginOptions = {
sortPosts: 'descending',
};
export const PluginOptionSchema = Joi.object<PluginOptions>({
const PluginOptionSchema = Joi.object<PluginOptions>({
path: Joi.string().default(DEFAULT_OPTIONS.path),
archiveBasePath: Joi.string()
.default(DEFAULT_OPTIONS.archiveBasePath)
@ -125,4 +126,15 @@ export const PluginOptionSchema = Joi.object<PluginOptions>({
sortPosts: Joi.string()
.valid('descending', 'ascending')
.default(DEFAULT_OPTIONS.sortPosts),
});
}).default(DEFAULT_OPTIONS);
export function validateOptions({
validate,
options,
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
const validatedOptions = validate(
PluginOptionSchema,
options,
) as PluginOptions;
return validatedOptions;
}