import * as _ from 'lodash' ; import * as fs from 'fs' ; import { parseDocument } from 'yaml' ; import { YAMLError } from 'yaml/util' ; import { IDescription , IModuleDocumentation , IModuleMetadata , IOption , } from '../interfaces/module' ; import { hasOwnProperty , isObject } from './misc' ; import { IPluginRoute , IPluginRoutesByName , IPluginRoutesByType , IPluginTypes , } from '../interfaces/pluginRouting' ; const DOCUMENTATION = 'DOCUMENTATION' ; export function processDocumentationFragments ( module , docFragments ) { module . fragments = [ ] ; const mainDocumentationFragment = module . rawDocumentationFragments . get ( DOCUMENTATION ) ; if ( mainDocumentationFragment && hasOwnProperty ( mainDocumentationFragment , 'extends_documentation_fragment' ) ) { const docFragmentNames = mainDocumentationFragment . extends_documentation_fragment instanceof Array ? mainDocumentationFragment . extends_documentation_fragment : [ mainDocumentationFragment . extends_documentation_fragment ] ; const resultContents = { } ; for ( const docFragmentName of docFragmentNames ) { const fragmentNameArray = docFragmentName . split ( '.' ) ; let fragmentPartName ; if ( fragmentNameArray . length === 2 || fragmentNameArray . length === 4 ) { fragmentPartName = fragmentNameArray . pop ( ) ?. toUpperCase ( ) as string ; } else { fragmentPartName = DOCUMENTATION ; } const docFragmentCatalogueName = fragmentNameArray . join ( '.' ) ; const docFragment = docFragments . get ( docFragmentCatalogueName ) || docFragments . get ( `ansible.builtin.${ docFragmentCatalogueName }` ) ; if ( docFragment && docFragment . rawDocumentationFragments . has ( fragmentPartName ) ) { module . fragments . push ( docFragment ) ; _ . mergeWith ( resultContents , docFragment . rawDocumentationFragments . get ( fragmentPartName ) , docFragmentMergeCustomizer ) ; } } _ . mergeWith ( resultContents , mainDocumentationFragment , docFragmentMergeCustomizer ) ; module . rawDocumentationFragments . set ( DOCUMENTATION , resultContents ) ; } } function docFragmentMergeCustomizer ( objValue , srcValue , key ) { if ( [ 'notes' , 'requirements' , 'seealso' ] . includes ( key ) && _ . isArray ( objValue ) ) { return objValue . concat ( srcValue ) ; } } export function processRawDocumentation ( moduleDocParts ) { const rawDoc = moduleDocParts . get ( DOCUMENTATION ) ; if ( rawDoc && typeof rawDoc . module === 'string' ) { const moduleDoc = { module : rawDoc . module , options : processRawOptions ( rawDoc . options ) , deprecated : ! ! rawDoc . deprecated , } ; if ( isIDescription ( rawDoc . short_description ) ) moduleDoc . shortDescription = rawDoc . short_description ; if ( isIDescription ( rawDoc . description ) ) moduleDoc . description = rawDoc . description ; if ( typeof rawDoc . version_added === 'string' ) moduleDoc . versionAdded = rawDoc . version_added ; if ( isIDescription ( rawDoc . author ) ) moduleDoc . author = rawDoc . author ; if ( isIDescription ( rawDoc . requirements ) ) moduleDoc . requirements = rawDoc . requirements ; if ( typeof rawDoc . seealso === 'object' ) moduleDoc . seealso = rawDoc . seealso as Record < string , unknown > ; if ( isIDescription ( rawDoc . notes ) ) moduleDoc . notes = rawDoc . notes ; return moduleDoc ; } } export function processRawOptions ( rawOptions ) { const options = new Map < string , IOption > ( ) ; if ( isObject ( rawOptions ) ) { for ( const [ optionName , rawOption ] of Object . entries ( rawOptions ) ) { if ( isObject ( rawOption ) ) { const optionDoc = { name : optionName , required : ! ! rawOption . required , default : rawOption . default , suboptions : processRawOptions ( rawOption . suboptions ) , } ; if ( isIDescription ( rawOption . description ) ) optionDoc . description = rawOption . description ; if ( rawOption . choices instanceof Array ) optionDoc . choices = rawOption . choices ; if ( typeof rawOption . type === 'string' ) optionDoc . type = rawOption . type ; if ( typeof rawOption . elements === 'string' ) optionDoc . elements = rawOption . elements ; if ( rawOption . aliases instanceof Array ) optionDoc . aliases = rawOption . aliases ; if ( typeof rawOption . version_added === 'string' ) optionDoc . versionAdded = rawOption . version_added ; options . set ( optionName , optionDoc ) ; if ( optionDoc . aliases ) { for ( const alias of optionDoc . aliases ) { options . set ( alias , optionDoc ) ; } } } } } return options ; } function isIDescription ( obj ) { return ( obj instanceof Array || typeof obj === 'string' ) ; } export function parseRawRouting ( rawDoc ) { const routesByType = new Map < IPluginTypes , IPluginRoutesByName > ( ) ; if ( hasOwnProperty ( rawDoc , 'plugin_routing' ) && isObject ( rawDoc . plugin_routing ) ) { for ( const [ pluginType , rawRoutesByName ] of Object . entries ( rawDoc . plugin_routing ) ) { if ( pluginType === 'modules' && isObject ( rawRoutesByName ) ) { routesByType . set ( pluginType , parseRawRoutesByName ( rawRoutesByName ) ) ; } } } return routesByType ; } function parseRawRoutesByName ( rawRoutesByName ) { const routesByName = new Map < string , IPluginRoute > ( ) ; for ( const [ moduleName , rawRoute ] of Object . entries ( rawRoutesByName ) ) { if ( isObject ( rawRoute ) ) routesByName . set ( moduleName , parseRawRoute ( rawRoute ) ) ; } return routesByName ; } function parseRawRoute ( rawRoute ) { const route = { } ; if ( isObject ( rawRoute . deprecation ) ) { route . deprecation = parseRawDepracationOrTombstone ( rawRoute . deprecation ) ; } if ( isObject ( rawRoute . tombstone ) ) { route . tombstone = parseRawDepracationOrTombstone ( rawRoute . tombstone ) ; } if ( typeof rawRoute . redirect === 'string' ) { route . redirect = rawRoute . redirect ; } return route ; } function parseRawDepracationOrTombstone ( rawInfo ) { let warningText ; let removalDate ; let removalVersion ; if ( typeof rawInfo . warning_text === 'string' ) { warningText = rawInfo . warning_text ; } if ( typeof rawInfo . removal_date === 'string' ) { removalDate = rawInfo . removal_date ; } if ( typeof rawInfo . removal_version === 'string' ) { removalVersion = rawInfo . removal_version ; } return { warningText : warningText , removalDate : removalDate , removalVersion : removalVersion , } ; } export class LazyModuleDocumentation implements IModuleMetadata { public static docsRegex = /(?<pre>[ \t]*(?<name>[A-Z0-9_]+)\s*=\s*r?(?<quotes>'''|""")(?:\n---)?\n?)(?<doc>.*?)\k<quotes>/gs ; source : string ; sourceLineRange : [ number , number ] = [ 0 , 0 ] ; fqcn : string ; namespace : string ; collection : string ; name : string ; errors : YAMLError [ ] = [ ] ; private _contents : Map < string , Record < string , unknown > > | undefined ; constructor ( source , fqcn , namespace , collection , name ) { this . source = source ; this . fqcn = fqcn ; this . namespace = namespace ; this . collection = collection ; this . name = name ; } public get rawDocumentationFragments ( ) : Map < string , Record < string , unknown > > { if ( ! this . _contents ) { this . _contents = new Map < string , Record < string , unknown > > ( ) ; const contents = fs . readFileSync ( this . source , { encoding : 'utf8' } ) ; let m ; while ( ( m = LazyModuleDocumentation . docsRegex . exec ( contents ) ) !== null ) { if ( m && m . groups && m . groups . name && m . groups . doc && m . groups . pre ) { if ( m . groups . name === DOCUMENTATION ) { let startLine = contents . substr ( 0 , m . index ) . match ( /\n/g ) ?. length || 0 ; startLine += m . groups . pre . match ( /\n/g ) ?. length || 0 ; const endLine = startLine + ( m . groups . doc . match ( /\n/g ) ?. length || 0 ) ; this . sourceLineRange = [ startLine , endLine ] ; } const document = parseDocument ( m . groups . doc ) ; this . _contents . set ( m . groups . name , document . toJSON ( ) ) ; this . errors = document . errors ; } } } return this . _contents ; } public set rawDocumentationFragments ( value ) { this . _contents = value ; } }