diff --git a/schemas/docfx.schema.json b/schemas/docfx.schema.json new file mode 100644 index 00000000000..114e39d382b --- /dev/null +++ b/schemas/docfx.schema.json @@ -0,0 +1,783 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json", + "title": "JSON Schema for docfx configuration file.", + "type": "object", + "additionalProperties": false, + "properties": { + "build": { + "$ref": "#/$defs/buildConfig" + }, + "metadata": { + "$ref": "#/$defs/metadataConfig" + }, + "merge": { + "$ref": "#/$defs/mergeConfig" + }, + "rules": { + "$ref": "#/$defs/ruleConfig" + } + }, + "$defs": { + "buildConfig": { + "title": "BuildJsonConfig", + "type": "object", + "description": "Build section defines configuration values for the build command.", + "additionalProperties": false, + "properties": { + "content": { + "$ref": "#/$defs/contentFileMapping" + }, + "resource": { + "$ref": "#/$defs/resourceFileMapping" + }, + "overwrite": { + "$ref": "#/$defs/overwriteFileMapping" + }, + "xref": { + "$ref": "#/$defs/xref" + }, + "dest": { + "type": "string", + "description": "(Deprecated) Defines the output folder of the generated build files.", + "deprecated": true + }, + "output": { + "type": "string", + "description": "Defines the output folder of the generated build files." + }, + "globalMetadata": { + "$ref": "#/$defs/globalMetadata" + }, + "globalMetadataFiles": { + "$ref": "#/$defs/globalMetadataFiles" + }, + "fileMetadata": { + "$ref": "#/$defs/fileMetadata" + }, + "fileMetadataFiles": { + "$ref": "#/$defs/fileMetadataFiles" + }, + "template": { + "$ref": "#/$defs/template" + }, + "theme": { + "$ref": "#/$defs/theme" + }, + "postProcessors": { + "$ref": "#/$defs/postProcessors" + }, + "debug": { + "type": "boolean", + "default": false, + "description": "Run in debug mode." + }, + "debugOutput": { + "type": "string", + "description": "The output folder for files generated for debugging purpose when in debug mode." + }, + "exportRawModel": { + "type": "boolean", + "default": false, + "description": "If set to true, data model to run template script will be extracted in .raw.model.json extension." + }, + "rawModelOutputFolder": { + "type": "string", + "description": "Specify the output folder for the raw model." + }, + "exportViewModel": { + "type": "boolean", + "default": false, + "description": "If set to true, data model to apply template will be extracted in .view.model.json extension." + }, + "viewModelOutputFolder": { + "type": "string", + "description": "Specify the output folder for the view model." + }, + "dryRun": { + "type": "boolean", + "default": false, + "description": "If set to true, template will not be actually applied to the documents." + }, + "maxParallelism": { + "type": "integer", + "description": "Set the max parallelism, 0 is auto." + }, + "markdownEngineProperties": { + "$ref": "#/$defs/markdownEngineProperties" + }, + "customLinkResolver": { + "type": "string", + "description": "Set the name of ICustomHrefGenerator derived class." + }, + "groups": { + "$ref": "#/$defs/groups" + }, + "sitemap": { + "$ref": "#/$defs/sitemap" + }, + "disableGitFeatures": { + "type": "boolean", + "default": false, + "description": "Disable fetching Git related information for articles." + } + } + }, + "contentFileMapping": { + "title": "Content", + "description": "Contains all the files to generate documentation, including metadata yml files and conceptual md files.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/fileMappingItem" + } + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/fileMappingItem" + } + ] + }, + "resourceFileMapping": { + "title": "Resource", + "description": "Contains all the resource files that conceptual and metadata files dependent on, e.g. image files.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/fileMappingItem" + } + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/fileMappingItem" + } + ] + }, + "overwriteFileMapping": { + "title": "Overwrite", + "description": "Contains all the conceptual files which contains yaml header with uid and is intended to override the existing metadata yml files.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/fileMappingItem" + } + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/fileMappingItem" + } + ] + }, + "fileMappingItem": { + "type": "object", + "description": "FileMappingItem", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The name of current item, the value is not used for now" + }, + "files": { + "$ref": "#/$defs/fileItems" + }, + "exclude": { + "$ref": "#/$defs/excludeFileItems" + }, + "src": { + "type": "string", + "description": "`src` defines the root folder for the source files." + }, + "dest": { + "type": "string", + "description": "The destination folder for the files if copy/transform is used." + }, + "group": { + "type": "string", + "description": "Group name for the current file-mapping item." + }, + "rootTocPath": { + "type": "string", + "description": "The Root TOC Path used for navbar in current group, relative to output root." + }, + "case": { + "type": "boolean", + "description": "Pattern match will be case sensitive." + }, + "noNegate": { + "type": "boolean", + "description": "Disable pattern begin with `!` to mean negate." + }, + "noExpand": { + "type": "boolean", + "description": "Disable `{a,b}c` => `[\"ac\", \"bc\"]`." + }, + "noEscape": { + "type": "boolean", + "description": "Disable the usage of `\\` to escape values." + }, + "noGlobStar": { + "type": "boolean", + "description": "Disable the usage of `**` to match everything including `/` when it is the beginning of the pattern or is after `/`." + }, + "dot": { + "type": "boolean", + "description": "Allow files start with `.` to be matched even if `.` is not explicitly specified in the pattern." + } + } + }, + "fileItems": { + "description": "The file glob pattern collection, with path relative to property `src` is value is set.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] + }, + "excludeFileItems": { + "description": "The file glob pattern collection for files that should be excluded, with path relative to property `src` is value is set.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] + }, + "globalMetadata": { + "type": "object", + "description": "Contains metadata that will be applied to every file, in key-value pair format.", + "additionalProperties": true + }, + "globalMetadataFiles": { + "description": "Specify a list of JSON file path containing globalMetadata settings.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] + }, + "xref": { + "description": "Specifies the urls of xrefmap used by content files. Supports local file path and HTTP/HTTPS urls.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] + }, + "fileMetadata": { + "type": "object", + "description": "Metadata that applies to some specific files.", + "additionalProperties": true + }, + "fileMetadataFiles": { + "description": "Specify a list of JSON file path containing fileMetadata settings.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] + }, + "template": { + "description": "The templates applied to each file in the documentation.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] + }, + "theme": { + "description": "The themes applied to the documentation.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] + }, + "postProcessors": { + "description": "Specify PostProcessor array. Build-in HtmlProcessor is automatically added by default.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] + }, + "groups": { + "type": "object", + "description": "Specifies the output folder and metadata of specified group name.", + "additionalProperties": { + "type": "object", + "additionalProperties": true, + "properties": { + "dest": { + "type": "string", + "description": "Defines the output folder of the generated build files." + } + } + } + }, + "sitemap": { + "type": "object", + "description": "Specifies the options for the sitemap.xml file.", + "properties": { + "baseUrl": { + "type": "string", + "format": "uri", + "description": "Base URL for the website. It should start with http or https." + }, + "changefreq": { + "$ref": "#/$defs/changefreq" + }, + "priority": { + "type": "number", + "default": 0.5, + "minimum": 0.0, + "maximum": 1.0, + "description": "the priority of this URL relative to other URLs on your site. Valid values range from 0.0 to 1.0." + }, + "lastmod": { + "type": "string", + "description": "The date of last modification of the page. If not specified, docfx sets the date to the build time." + }, + "fileOptions": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "baseUrl": { + "type": "string", + "format": "uri", + "description": "Base URL for the website. It should start with http or https." + }, + "changefreq": { + "$ref": "#/$defs/changefreq" + }, + "priority": { + "type": "number", + "default": 0.5, + "minimum": 0.0, + "maximum": 1.0, + "description": "the priority of this URL relative to other URLs on your site. Valid values range from 0.0 to 1.0." + }, + "lastmod": { + "type": "string", + "description": "The date of last modification of the page. If not specified, docfx sets the date to the build time." + } + } + } + } + } + }, + "changefreq": { + "type": "string", + "description": "Determines how frequently the page is likely to change. Valid values are always, hourly, daily, weekly, monthly, yearly, never.", + "default": "daily", + "enum": [ + "always", + "hourly", + "daily", + "weekly", + "monthly", + "yearly", + "never" + ] + }, + "markdownEngineProperties": { + "description": "Set the parameters for markdown engine, value should be a JSON string.", + "type": "object", + "properties": { + "enableSourceInfo": { + "type": "boolean", + "default": true, + "description": "Enables line numbers" + }, + "markdigExtensions": { + "description": "List of optional Markdig extensions to add or modify settings.", + "type": "array", + "items": { + "$ref": "#/$defs/markdigExtensionSetting" + } + }, + "fallbackFolders": { + "description": "Fallback folders", + "type": "array", + "items": { + "type": "string" + } + }, + "alerts": { + "title": "Alerts", + "description": "Alert keywords in markdown without the surrounding [!] and the corresponding CSS class names.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "plantUml": { + "$ref": "#/$defs/plantUmlOptions" + } + } + }, + "markdigExtensionSetting": { + "description": "Markdig extension setting.", + "anyOf": [ + { + "type": "string", + "description": "String" + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "markdig extension name", + "additionalProperties": {} + } + } + } + ] + }, + "plantUmlOptions": { + "type": "object", + "description": "PlantUml extension configuration parameters", + "properties": { + "javaPath": { + "type": "string", + "description": "" + }, + "remoteUrl": { + "type": "string", + "description": "" + }, + "localPlantUmlPath": { + "type": "string", + "description": "" + }, + "localGraphvizDotPath": { + "type": "string", + "description": "" + }, + "renderingMode": { + "type": "string", + "description": "", + "enum": [ + "Remote", + "Local" + ] + }, + "delimitor": { + "type": "string", + "description": "" + }, + "outputFormat": { + "type": "string", + "description": "", + "enum": [ + "Png", + "Svg", + "Eps", + "Pdf", + "Vdx", + "Xmi", + "Scxml", + "Html", + "Ascii", + "Ascii_Unicode", + "LaTeX" + ] + } + } + }, + "metadataConfig": { + "type": "array", + "items": { + "type": "object", + "properties": { + "src": { + "$ref": "#/$defs/srcFileMapping" + }, + "dest": { + "type": "string", + "description": "(Deprecated) Defines the output folder of the generated metadata files.", + "deprecated": true + }, + "output": { + "type": "string", + "description": "Defines the output folder of the generated metadata files." + }, + "outputFormat": { + "type": "string", + "description": "Defines the output file format.", + "default": "mref", + "enum": [ + "mref", + "markdown", + "apiPage" + ] + }, + "shouldSkipMarkup": { + "type": "boolean", + "default": false, + "description": "If set to true, DocFX would not render triple-slash-comments in source code as markdown." + }, + "references": { + "$ref": "#/$defs/referencesFileMapping", + "description": "Specify additinal assembly reference files." + }, + "filter": { + "type": "string", + "description": "Defines the filter configuration file." + }, + "includePrivateMembers": { + "type": "boolean", + "default": false, + "description": "Include private or internal APIs." + }, + "includeExplicitInterfaceImplementations": { + "type": "boolean", + "default": false, + "description": "Include explicit interface implementations." + }, + "globalNamespaceId": { + "type": "string", + "description": "Specify the name to use for the global namespace." + }, + "properties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "disableGitFeatures": { + "type": "boolean", + "default": false, + "description": "Disables generation of view source links." + }, + "codeSourceBasePath": { + "type": "string", + "description": "Specify the base directory that is used to resolve code source." + }, + "disableDefaultFilter": { + "type": "boolean", + "default": false, + "description": "Disables the default filter configuration file." + }, + "noRestore": { + "type": "boolean", + "default": false, + "description": "Do not run dotnet restore before building the projects." + }, + "namespaceLayout": { + "type": "string", + "description": "Defines how namespaces in TOC are organized.", + "default": "flattened", + "enum": [ + "flattened", + "nested" + ] + }, + "memberLayout": { + "type": "string", + "description": "Defines how member pages are organized.", + "default": "samePage", + "enum": [ + "samePage", + "separatePages" + ] + }, + "enumSortOrder": { + "type": "string", + "description": "Defines enum sort orders.", + "default": "alphabetic", + "enum": [ + "alphabetic", + "declaringOrder" + ] + }, + "allowCompilationErrors": { + "type": "boolean", + "default": false, + "description": "When enabled, continues documentation generation in case of compilation errors." + } + } + } + }, + "srcFileMapping": { + "description": "Defines the source projects to have metadata generated.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/fileMappingItem" + } + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/fileMappingItem" + } + ] + }, + "referencesFileMapping": { + "description": "Specify additinal assembly reference files.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/fileMappingItem" + } + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/fileMappingItem" + } + ] + }, + "mergeConfig": { + "type": "array", + "items": { + "type": "object", + "properties": { + "content": { + "$ref": "#/$defs/contentFileMapping" + }, + "dest": { + "type": "string", + "description": "Defines the output folder of the generated merge files." + }, + "globalMetadata": { + "type": "object", + "description": "Contains metadata that will be applied to every file, in key-value pair format.", + "additionalProperties": {} + }, + "fileMetadata": { + "$ref": "#/$defs/fileMetadata" + }, + "tocMetadata": { + "description": "Metadata that applies to toc files.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] + } + } + } + }, + "ruleConfig": { + "type": "object", + "additionalProperties": { + "enum": [ + "verbose", + "info", + "suggestion", + "warning", + "error", + "diagnostic" + ] + } + } + } +} diff --git a/schemas/filterconfig.schema.json b/schemas/filterconfig.schema.json new file mode 100644 index 00000000000..10e362ffdc1 --- /dev/null +++ b/schemas/filterconfig.schema.json @@ -0,0 +1,97 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/filterconfig.schema.json", + "title": "JSON Schema for docfx TOC file.", + "type": "object", + "additionalProperties": false, + "properties": { + "apiRules": { + "type": "array", + "description": "Include/exclude rules using uid.", + "items": { + "$ref": "#/$defs/configFilterRuleItemUnion" + } + }, + "attributeRules": { + "type": "array", + "description": "Include/exclude rules using attribute.", + "items": { + "$ref": "#/$defs/configFilterRuleItemUnion" + } + } + }, + "$defs": { + "configFilterRuleItemUnion": { + "type": "object", + "additionalProperties": false, + "properties": { + "include": { + "type": "object", + "properties": { + "uidRegex": { + "type": "string" + }, + "kind": { + "$ref": "#/$defs/extendedSymbolKind" + }, + "attribute": { + "$ref": "#/$defs/attributeFilterInfo" + } + } + }, + "exclude": { + "type": "object", + "properties": { + "uidRegex": { + "type": "string" + }, + "kind": { + "$ref": "#/$defs/extendedSymbolKind" + }, + "attribute": { + "$ref": "#/$defs/attributeFilterInfo" + } + } + } + } + }, + "extendedSymbolKind": { + "enum": [ + "assembly", + "namespace", + "class", + "struct", + "enum", + "interface", + "delegate", + "type", + "event", + "field", + "method", + "property", + "member" + ] + }, + "attributeFilterInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "uid": { + "type": "string" + }, + "ctorArguments": { + "type": "array", + "items": { + "type": "string" + } + }, + "ctorNamedArguments": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } +} diff --git a/schemas/toc.schema.json b/schemas/toc.schema.json new file mode 100644 index 00000000000..90d97035294 --- /dev/null +++ b/schemas/toc.schema.json @@ -0,0 +1,105 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/toc.schema.json", + "title": "JSON Schema for docfx TOC file.", + "anyOf": [ + { + "$ref": "#/$defs/tocItem" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/tocItem" + } + } + ], + "$defs": { + "tocItem": { + "type": "object", + "additionalProperties": true, + "properties": { + "uid": { + "type": "string", + "description": "The uid of the article. Can be used instead of href." + }, + "name": { + "type": "string", + "description": "" + }, + "displayName": { + "type": "string", + "description": "An optional display name for the TOC node. When not specified, uses the title metadata or the first Heading 1 element from the referenced article as the display name." + }, + "href": { + "type": "string", + "description": "The path the TOC node leads to. Optional because a node can exist just to parent other nodes." + }, + "originalHref": { + "type": "string", + "description": "" + }, + "tocHref": { + "type": "string", + "description": "" + }, + "originalTocHref": { + "type": "string", + "description": "" + }, + "topicHref": { + "type": "string", + "description": "Specify the topic href of the TOC Item. It is useful when href is linking to a folder or TOC file or tocHref is used." + }, + "originalTopicHref": { + "type": "string", + "description": "" + }, + "includedFrom": { + "type": "string", + "description": "" + }, + "homepage": { + "type": "string", + "description": "(Deprecated)", + "deprecated": true + }, + "originalHomepage": { + "type": "string", + "description": "(Deprecated).", + "deprecated": true + }, + "homepageUid": { + "type": "string", + "description": "(Deprecated).", + "deprecated": true + }, + "topicUid": { + "type": "string", + "description": "Specify the uid of the referenced file. If the value is set, it overwrites the value of topicHref." + }, + "order": { + "type": "integer", + "default": 0, + "description": "Specify the order of toc item, TOCs with a smaller order value are picked first." + }, + "items": { + "type": "array", + "description": "List of TOC items.", + "items": { + "$ref": "#/$defs/tocItem" + } + }, + "expanded": { + "type": "boolean", + "default": false, + "description": "If set to true, Child items are displayed as expanded." + }, + "dropdown": { + "type": "boolean", + "default": false, + "description": "If set to true, Child items are displayed as dropdown on top navigation bar." + } + } + } + } +} diff --git a/schemas/xrefmap.schema.json b/schemas/xrefmap.schema.json new file mode 100644 index 00000000000..c79672c96f0 --- /dev/null +++ b/schemas/xrefmap.schema.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/xrefmap.schema.json", + "title": "JSON Schema for docfx xrefmap file.", + "type": "object", + "additionalProperties": true, + "properties": { + "sorted": { + "type": "boolean", + "default": false, + "description": "Indicate references are sorted by uid or not." + }, + "hrefUpdated": { + "type": "boolean", + "default": false, + "description": "Indicate href links are updated or not." + }, + "baseUrl": { + "type": "string", + "format": "uri", + "description": "Base url. It's used when href is specified as relative url." + }, + "redirections": { + "type": "array", + "description": "List of XRefMapRedirection items.", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "uidPrefix": { + "type": "string", + "description": "Prefix of the UID to redirect." + }, + "href": { + "type": "string", + "format": "uri-reference", + "description": "URL to redirect." + } + } + } + }, + "references": { + "type": "array", + "description": "List of XRefSpec items.", + "items": { + "$ref": "#/$defs/xrefSpec" + } + } + }, + "$defs": { + "xrefSpec": { + "type": "object", + "description": "", + "additionalProperties": true, + "properties": { + "uid": { + "type": "string", + "description": "UID to a conceptual topic or API reference." + }, + "name": { + "type": "string", + "description": "Title of the topic." + }, + "href": { + "type": "string", + "description": "URL to the topic, which is an absolute url or relative path to current file (xrefmap.yml)" + }, + "fullName": { + "type": "string", + "description": "The fully qualified name of API. For example, for String class, its name is String and fully qualified name is System.String. This property is not used in link title resolve for now but reserved for future use." + }, + "nameWithType": { + "type": "string", + "description": "Display name of type." + }, + "commentId": { + "type": "string", + "description": "The id of API comment." + }, + "isSpec": { + "type": "string", + "description": "" + } + } + } + } +} diff --git a/src/Docfx.Common/FileMappingItem.cs b/src/Docfx.Common/FileMappingItem.cs index bd51811df11..99454ba40d8 100644 --- a/src/Docfx.Common/FileMappingItem.cs +++ b/src/Docfx.Common/FileMappingItem.cs @@ -19,21 +19,21 @@ public class FileMappingItem public string Name { get; set; } /// - /// The file glob pattern collection, with path relative to property `src`/`cwd` is value is set + /// The file glob pattern collection, with path relative to property `src` is value is set /// [JsonProperty("files")] [JsonPropertyName("files")] public FileItems Files { get; set; } /// - /// The file glob pattern collection for files that should be excluded, with path relative to property `src`/`cwd` is value is set + /// The file glob pattern collection for files that should be excluded, with path relative to property `src` is value is set /// [JsonProperty("exclude")] [JsonPropertyName("exclude")] public FileItems Exclude { get; set; } /// - /// `src` defines the root folder for the source files, it has the same meaning as `cwd` + /// `src` defines the root folder for the source files. /// [JsonProperty("src")] [JsonPropertyName("src")] diff --git a/src/Docfx.DataContracts.Common/Constants.cs b/src/Docfx.DataContracts.Common/Constants.cs index d481b7ba86e..270982e7508 100644 --- a/src/Docfx.DataContracts.Common/Constants.cs +++ b/src/Docfx.DataContracts.Common/Constants.cs @@ -114,6 +114,14 @@ public static class TableOfContents public const string YamlTocFileName = "toc.yml"; } + public static class JsonSchemas + { + public const string Docfx = "schemas/docfx.schema.json"; + public const string Toc = "schemas/toc.schema.json"; + public const string XrefMap = "schemas/xrefmap.schema.json"; + public const string FilterConfig = "schemas/filterconfig.schema.json"; + } + public static class EnvironmentVariables { #pragma warning disable format diff --git a/test/docfx.Tests/Api.verified.cs b/test/docfx.Tests/Api.verified.cs index a18ce4c6167..5293d77ab65 100644 --- a/test/docfx.Tests/Api.verified.cs +++ b/test/docfx.Tests/Api.verified.cs @@ -2383,6 +2383,13 @@ public static class ExtensionMemberPrefix public const string Source = "source."; public const string Spec = "spec."; } + public static class JsonSchemas + { + public const string Docfx = "schemas/docfx.schema.json"; + public const string FilterConfig = "schemas/filterconfig.schema.json"; + public const string Toc = "schemas/toc.schema.json"; + public const string XrefMap = "schemas/xrefmap.schema.json"; + } public static class MetadataName { public const string Version = "version"; diff --git a/test/docfx.Tests/Assets/docfx.json_build/docfx.json b/test/docfx.Tests/Assets/docfx.json_build/docfx.json index 5cc41b5b839..ffbaaa6fe96 100644 --- a/test/docfx.Tests/Assets/docfx.json_build/docfx.json +++ b/test/docfx.Tests/Assets/docfx.json_build/docfx.json @@ -1,22 +1,21 @@ { "build": { // input could be YAML or MARKDOWN files, outputs are Final-YAML files - "content": - [ - { - "files": ["**/*.yml"], - "cwd": "obj/docfx" - }, - { - "files": ["tutorial/**/*.md", "spec/**/*.md"] - }, - { - "files": ["toc.yml"] - } - ], + "content": [ + { + "files": [ "**/*.yml" ], + "src": "obj/docfx" + }, + { + "files": [ "tutorial/**/*.md", "spec/**/*.md" ] + }, + { + "files": [ "toc.yml" ] + } + ], "resource": [ - { - "files": ["images/**"] - } + { + "files": [ "images/**" ] + } ], "globalMetadata": { "key": "value" @@ -26,17 +25,16 @@ "filepattern1": "string", "filePattern2": 2, "filePattern3": true, - "filePattern4": [ ], - "filePattern5": { } + "filePattern4": [], + "filePattern5": {} } }, "overwrite": "apispec/*.md", - "externalReference": [ + "xref": [ "external/*.yml.zip" ], "dest": "_site", "template": "default", - "theme": "happy", - "title": "Doc-as-code documentation" + "theme": "happy" } -} \ No newline at end of file +} diff --git a/test/docfx.Tests/Assets/docfx.json_empty/docfx.json b/test/docfx.Tests/Assets/docfx.json_empty/docfx.json index 8593c62d965..0f530c147cf 100644 --- a/test/docfx.Tests/Assets/docfx.json_empty/docfx.json +++ b/test/docfx.Tests/Assets/docfx.json_empty/docfx.json @@ -1,2 +1,2 @@ { -} \ No newline at end of file +} diff --git a/test/docfx.Tests/Assets/docfx.json_invalid_key/docfx.json b/test/docfx.Tests/Assets/docfx.json_invalid_key/docfx.json index 9b054bc98f6..4453b55bdb3 100644 --- a/test/docfx.Tests/Assets/docfx.json_invalid_key/docfx.json +++ b/test/docfx.Tests/Assets/docfx.json_invalid_key/docfx.json @@ -1,3 +1,3 @@ { - "invalid": { } -} \ No newline at end of file + "invalid": {} +} diff --git a/test/docfx.Tests/Assets/docfx.json_metadata/docfx.json b/test/docfx.Tests/Assets/docfx.json_metadata/docfx.json index 38be66b74d3..9bf9640cf08 100644 --- a/test/docfx.Tests/Assets/docfx.json_metadata/docfx.json +++ b/test/docfx.Tests/Assets/docfx.json_metadata/docfx.json @@ -3,9 +3,9 @@ { "src": [ { - "files": ["**/*.csproj"], - "exclude": [ "**/bin/**", "**/obj/**" ], // `exclude` is also relative to `cwd` - "cwd": "../src" + "files": [ "**/*.csproj" ], + "exclude": [ "**/bin/**", "**/obj/**" ], // `exclude` is also relative to `src` + "src": "../src" } ], "dest": "obj/docfx/api/dotnet" @@ -13,11 +13,11 @@ { "src": [ { - "files": ["**/*.js"], - "cwd": "../src" + "files": [ "**/*.js" ], + "src": "../src" } ], "dest": "obj/docfx/api/js" // throw error when dest is not unique } ] -} \ No newline at end of file +} diff --git a/test/docfx.Tests/Assets/docfx.json_metadata/docfxWithFilter.json b/test/docfx.Tests/Assets/docfx.json_metadata/docfxWithFilter.json index 442ab519054..69f8dde8f2d 100644 --- a/test/docfx.Tests/Assets/docfx.json_metadata/docfxWithFilter.json +++ b/test/docfx.Tests/Assets/docfx.json_metadata/docfxWithFilter.json @@ -4,7 +4,7 @@ "src": [ { "files": [ "**/*.csproj" ], - "exclude": [ "**/bin/**", "**/obj/**" ], // `exclude` is also relative to `cwd` + "exclude": [ "**/bin/**", "**/obj/**" ], // `exclude` is also relative to `src` "src": "../src" } ], @@ -12,4 +12,4 @@ "filter": "filter.yaml" } ] -} \ No newline at end of file +} diff --git a/test/docfx.Tests/Assets/docfx.json_metadata_build/docfx.json b/test/docfx.Tests/Assets/docfx.json_metadata_build/docfx.json index a69ef626303..d6811fd99d8 100644 --- a/test/docfx.Tests/Assets/docfx.json_metadata_build/docfx.json +++ b/test/docfx.Tests/Assets/docfx.json_metadata_build/docfx.json @@ -3,9 +3,9 @@ { "src": [ { - "files": ["**/*.csproj"], - "exclude": [ "**/bin/**", "**/obj/**" ], // `exclude` is also relative to `cwd` - "cwd": "../src" + "files": [ "**/*.csproj" ], + "exclude": [ "**/bin/**", "**/obj/**" ], // `exclude` is also relative to `src` + "src": "../src" } ], "dest": "obj/docfx/api/dotnet" @@ -13,31 +13,30 @@ { "src": [ { - "files": ["**/*.js"], - "cwd": "../src" + "files": [ "**/*.js" ], + "src": "../src" } ], "dest": "obj/docfx/api/js" // throw error when dest is not unique } ], "build": { // input could be YAML or MARKDOWN files, outputs are Final-YAML files - "content": - [ - { - "files": ["**/*.yml"], - "cwd": "obj/docfx" - }, - { - "files": ["tutorial/**/*.md", "spec/**/*.md"] - }, - { - "files": ["toc.yml"] - } - ], + "content": [ + { + "files": [ "**/*.yml" ], + "src": "obj/docfx" + }, + { + "files": [ "tutorial/**/*.md", "spec/**/*.md" ] + }, + { + "files": [ "toc.yml" ] + } + ], "resource": [ - { - "files": ["images/**"] - } + { + "files": [ "images/**" ] + } ], "globalMetadata": { "key": "value" @@ -47,17 +46,16 @@ "filepattern1": "string", "filePattern2": 2, "filePattern3": true, - "filePattern4": [ ], - "filePattern5": { } + "filePattern4": [], + "filePattern5": {} } }, "overwrite": "apispec/*.md", - "externalReference": [ + "xref": [ "external/*.yml.zip" ], - "dest": "_site", + "output": "_site", "template": "default", - "theme": "happy", - "title": "Doc-as-code documentation" + "theme": "happy" } -} \ No newline at end of file +} diff --git a/test/docfx.Tests/Assets/docfx.sample.1.json b/test/docfx.Tests/Assets/docfx.sample.1.json index a69ef626303..ded53486622 100644 --- a/test/docfx.Tests/Assets/docfx.sample.1.json +++ b/test/docfx.Tests/Assets/docfx.sample.1.json @@ -4,8 +4,8 @@ "src": [ { "files": ["**/*.csproj"], - "exclude": [ "**/bin/**", "**/obj/**" ], // `exclude` is also relative to `cwd` - "cwd": "../src" + "exclude": [ "**/bin/**", "**/obj/**" ], // `exclude` is also relative to `src` + "src": "../src" } ], "dest": "obj/docfx/api/dotnet" @@ -14,7 +14,7 @@ "src": [ { "files": ["**/*.js"], - "cwd": "../src" + "src": "../src" } ], "dest": "obj/docfx/api/js" // throw error when dest is not unique @@ -25,7 +25,7 @@ [ { "files": ["**/*.yml"], - "cwd": "obj/docfx" + "src": "obj/docfx" }, { "files": ["tutorial/**/*.md", "spec/**/*.md"] @@ -52,12 +52,11 @@ } }, "overwrite": "apispec/*.md", - "externalReference": [ + "xref": [ "external/*.yml.zip" ], "dest": "_site", "template": "default", - "theme": "happy", - "title": "Doc-as-code documentation" + "theme": "happy" } -} \ No newline at end of file +} diff --git a/test/docfx.Tests/JsonSchemaTest.cs b/test/docfx.Tests/JsonSchemaTest.cs new file mode 100644 index 00000000000..4fa2c37aa15 --- /dev/null +++ b/test/docfx.Tests/JsonSchemaTest.cs @@ -0,0 +1,189 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Docfx.Common; +using Docfx.DataContracts.Common; +using Docfx.JsonSchemaGenerator.Tests; +using Docfx.Tests.Common; +using FluentAssertions; +using Json.Schema; +using Xunit.Abstractions; +using YamlDotNet.Serialization; + +namespace Docfx.Tests; + +[Collection("docfx STA")] +public class JsonSchemaTest : TestBase +{ + private readonly ITestOutputHelper output; + + public JsonSchemaTest(ITestOutputHelper output) + { + this.output = output; + } + + [Theory] + [InlineData("docs/docfx.json")] + [InlineData("samples/csharp/docfx.json")] + [InlineData("samples/extensions/docfx.json")] + [InlineData("samples/seed/docfx.json")] + [InlineData("test/docfx.Tests/Assets/docfx.json_build/docfx.json")] + [InlineData("test/docfx.Tests/Assets/docfx.json_empty/docfx.json")] + [InlineData("test/docfx.Tests/Assets/docfx.json_metadata/docfx.json")] + [InlineData("test/docfx.Tests/Assets/docfx.json_metadata/docfxWithFilter.json")] + [InlineData("test/docfx.Tests/Assets/docfx.json_metadata_build/docfx.json")] + public void JsonSchemaTest_Docfx_Json(string path) + { + // Arrange + var jsonElement = LoadAsJsonElement(path); + + // Act + var result = JsonSchemaUtility.ValidateJsonSchema(jsonElement, Constants.JsonSchemas.Docfx); + + // Assert + result.IsValid.Should().BeTrue(); + } + + [Theory] + [InlineData("test/docfx.Tests/Assets/docfx.json_invalid_format/docfx.json")] + [InlineData("test/docfx.Tests/Assets/docfx.json_invalid_key/docfx.json")] + public void JsonSchemaTest_Docfx_Json_Invalid(string path) + { + // Arrange + var jsonElement = LoadAsJsonElement(path); + + // Act + var result = JsonSchemaUtility.ValidateJsonSchema(jsonElement, Constants.JsonSchemas.Docfx); + + // Assert + result.IsValid.Should().BeFalse(); + } + + [Theory] + [InlineData("src/Docfx.Dotnet/Resources/defaultfilterconfig.yml")] + [InlineData("test/Docfx.Dotnet.Tests/TestData/filterconfig.yml")] + [InlineData("test/Docfx.Dotnet.Tests/TestData/filterconfig_attribute.yml")] + [InlineData("test/Docfx.Dotnet.Tests/TestData/filterconfig_docs_sample.yml")] + public void JsonSchemaTest_FilterConfig(string path) + { + // Arrange + var jsonElement = LoadAsJsonElement(path); + + // Act + var result = JsonSchemaUtility.ValidateJsonSchema(jsonElement, Constants.JsonSchemas.FilterConfig); + + // Assert + result.IsValid.Should().BeTrue(); + } + + [Theory] + [InlineData("test/docfx.Snapshot.Tests/SamplesTest.CSharp/api/toc.json.view.verified.json")] + [InlineData("test/docfx.Snapshot.Tests/SamplesTest.Extensions/api/toc.json.view.verified.json")] + [InlineData("test/docfx.Snapshot.Tests/SamplesTest.Extensions/toc.json.view.verified.json")] + [InlineData("test/docfx.Snapshot.Tests/SamplesTest.Seed/api/toc.json.view.verified.json")] + [InlineData("test/docfx.Snapshot.Tests/SamplesTest.Seed/apipage/toc.json.view.verified.json")] + [InlineData("test/docfx.Snapshot.Tests/SamplesTest.Seed/articles/toc.json.view.verified.json")] + [InlineData("test/docfx.Snapshot.Tests/SamplesTest.Seed/md/toc.json.view.verified.json")] + [InlineData("test/docfx.Snapshot.Tests/SamplesTest.Seed/pdf/toc.json.view.verified.json")] + [InlineData("test/docfx.Snapshot.Tests/SamplesTest.Seed/restapi/toc.json.view.verified.json")] + [InlineData("test/docfx.Snapshot.Tests/SamplesTest.Seed/toc.json.view.verified.json")] + public void JsonSchemaTest_Toc_Json(string path) + { + // Arrange + var jsonElement = LoadAsJsonElement(path); + + // Act + var result = JsonSchemaUtility.ValidateJsonSchema(jsonElement, Constants.JsonSchemas.Toc); + + // Assert + result.IsValid.Should().BeTrue(); + } + + [Theory] + [InlineData("test/Docfx.Build.RestApi.WithPlugins.Tests/TestData/swagger/toc.yml")] + [InlineData("test/docfx.Snapshot.Tests/SamplesTest.SeedMarkdown/toc.verified.yml")] + public void JsonSchemaTest_Toc_Yaml(string path) + { + // Arrange + var jsonElement = LoadAsJsonElement(path); + + // Act + var result = JsonSchemaUtility.ValidateJsonSchema(jsonElement, Constants.JsonSchemas.Toc); + + // Assert + result.IsValid.Should().BeTrue(); + } + + [Theory] + [InlineData("test/Docfx.Build.Tests/TestData/xrefmap.json")] + public void JsonSchemaTest_XrefMap_Json(string path) + { + // Arrange + var jsonElement = LoadAsJsonElement(path); + + // Act + var result = JsonSchemaUtility.ValidateJsonSchema(jsonElement, Constants.JsonSchemas.XrefMap); + + // Assert + result.IsValid.Should().BeTrue(); + } + + [Theory] + [InlineData("test/Docfx.Build.Tests/TestData/xrefmap.yml")] + [InlineData("test/docfx.Snapshot.Tests/SamplesTest.CSharp/xrefmap.verified.yml")] + [InlineData("test/docfx.Snapshot.Tests/SamplesTest.Extensions/xrefmap.verified.yml")] + [InlineData("test/docfx.Snapshot.Tests/SamplesTest.Seed/xrefmap.verified.yml")] + public void JsonSchemaTest_XrefMap_Yaml(string path) + { + // Arrange + var jsonElement = LoadAsJsonElement(path); + + // Act + var result = JsonSchemaUtility.ValidateJsonSchema(jsonElement, Constants.JsonSchemas.XrefMap); + + // Assert + result.IsValid.Should().BeTrue(); + } + + /// + /// Load file content as JsonElement. + /// + private static JsonElement LoadAsJsonElement(string path) + { + var solutionDir = PathHelper.GetSolutionFolder(); + + var filePath = Path.Combine(solutionDir, path); + + if (!File.Exists(filePath)) + throw new FileNotFoundException(filePath); + + switch (Path.GetExtension(filePath)) + { + case ".json": + var doc = JsonDocument.Parse(File.OpenRead(filePath), JsonSchemaUtility.DefaultJsonDocumentOptions); + return doc.RootElement; + case ".yml": + var yaml = File.ReadAllText(filePath); + var yamlObject = YamlUtility.Deserialize(new StringReader(yaml)); + + var serializer = new SerializerBuilder() + .JsonCompatible() + .Build(); + var json = serializer.Serialize(yamlObject); + return JsonSerializer.Deserialize(json); + + default: + throw new NotSupportedException(path); + } + } + + private void WriteFailedResultsDetails(EvaluationResults result) + { + if (result.IsValid) + return; + + var json = JsonSerializer.Serialize(result, JsonSerializerOptions.Default); + output.WriteLine(json); + } +} diff --git a/test/docfx.Tests/Utilities/JsonSchemaUtility.cs b/test/docfx.Tests/Utilities/JsonSchemaUtility.cs new file mode 100644 index 00000000000..c96f65aa433 --- /dev/null +++ b/test/docfx.Tests/Utilities/JsonSchemaUtility.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Json.Schema; +using System.Text; +using System.Text.Json; + +namespace Docfx.JsonSchemaGenerator.Tests; + +internal static class JsonSchemaUtility +{ + public static readonly JsonSerializerOptions DefaultSerializerOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + }; + + public static readonly EvaluationOptions DefaultEvaluationOptions = new EvaluationOptions + { + ValidateAgainstMetaSchema = false, + OutputFormat = OutputFormat.List, + }; + + public static readonly JsonDocumentOptions DefaultJsonDocumentOptions = new() + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip, + }; + + public static EvaluationResults ValidateJsonSchema(JsonElement jsonElement, string schemaPath) + { + var solutionDir = PathHelper.GetSolutionFolder(); + var jsonSchemaPath = Path.Combine(solutionDir, schemaPath); + + if (!File.Exists(jsonSchemaPath)) + throw new FileNotFoundException(jsonSchemaPath); + + var schema = JsonSchema.FromFile(jsonSchemaPath, DefaultSerializerOptions); + + var result = schema.Evaluate(jsonElement, DefaultEvaluationOptions); + return result; + } +} diff --git a/test/docfx.Tests/Utilities/PathHelper.cs b/test/docfx.Tests/Utilities/PathHelper.cs new file mode 100644 index 00000000000..d3f874cc3e9 --- /dev/null +++ b/test/docfx.Tests/Utilities/PathHelper.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +namespace Docfx.JsonSchemaGenerator.Tests; + +internal class PathHelper +{ + public static string GetSolutionFolder([CallerFilePath] string callerFilePath = "") + { + if (callerFilePath.StartsWith("/_/")) + { + // PathMap is rewritten on CI environment (`ContinuousIntegrationBuild=true`). + // So try to get workspace folder from GitHub Action environment variable. + var workspace = Environment.GetEnvironmentVariable("GITHUB_WORKSPACE"); + if (workspace != null) + return workspace; + } + + if (!File.Exists(callerFilePath)) + { + // CallerFilePath is resolved at build timing. + // If build/test is executed on separated machine. It failed to find file. + throw new Exception($"File is not found. callerFilePath: {callerFilePath}"); + } + + return FindSolutionFolder(callerFilePath, "docfx"); + } + + /// + /// Find docfx solution folder. + /// + private static string FindSolutionFolder(string callerFilePath, string solutionName) + { + var dir = new FileInfo(callerFilePath).Directory; + while (dir != null + && dir.Name != solutionName + && !dir.EnumerateFiles($"{solutionName}.sln").Any()) + { + dir = dir.Parent; + } + + if (dir == null) + throw new Exception("Failed to find solution folder."); + + return dir.FullName; + } +}