All files / modules/10-common/utils YamlUtils.ts

70.83% Statements 51/72
44.44% Branches 28/63
80% Functions 8/10
70.83% Lines 51/72

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171                  58x 58x 58x 58x 58x   58x                 58x   4x 4x   10x 35x 35x 35x 35x 35x 6x     6x   29x 1x       4x 4x                   58x         1x 1x                       58x 2x     2x     2x 2x 2x     58x                                   1x   1x       1x 1x 1x 1x 1x                                             1x       58x 2x 2x 2x     58x 1x                             58x 58x 58x 58x 58x 58x    
/*
 * Copyright 2021 Harness Inc. All rights reserved.
 * Use of this source code is governed by the PolyForm Shield 1.0.0 license
 * that can be found in the licenses directory at the root of this repository, also available at
 * https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt.
 */
 
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import * as yamlLanguageService from '@wings-software/monaco-yaml/lib/esm/languageservice/yamlLanguageService'
import { isEmpty } from 'lodash-es'
import { TextDocument, Diagnostic } from 'vscode-languageserver-types'
import { parse } from 'yaml'
import { yamlStringify } from '@common/utils/YamlHelperMethods'
 
const DEFAULT_YAML_PATH = 'DEFAULT_YAML_PATH'
 
/**
 * @description Find json path(s) of a given node in json from it's nearest parent
 * @param jsonObj json equivalent of yaml
 * @param leafNode leaf node whose path(s) from the nearest parent needs to be known
 * @param delimiter delimiter to be used in node path(s) from parent
 * @returns exactly matching json path in the tree
 */
const findLeafToParentPath = (jsonObj: Record<string, any>, leafNode: string, delimiter = '.'): string | undefined => {
  // to remove all leading non-characters
  const leaf = leafNode.replace(/^[^a-zA-Z]+/, '')
  const matchingPath: string[] = []
  function findPath(currJSONObj: Record<string, any>, currentDepth: number, previous?: string): void {
    Object.keys(currJSONObj).forEach((key: string) => {
      const value = currJSONObj[key]
      const type = Object.prototype.toString.call(value)
      const isObject = type === '[object Object]' || type === '[object Array]'
      const newKey = previous ? previous + delimiter + key : key
      if (isObject && Object.keys(value).length) {
        Iif (key.match(leaf)) {
          matchingPath.push(newKey)
        }
        return findPath(value, currentDepth + 1, newKey)
      }
      if (newKey.match(leaf)) {
        matchingPath.push(newKey)
      }
    })
  }
  findPath(jsonObj, 1)
  return matchingPath.length > 0 ? matchingPath.slice(-1).pop() : 'DEFAULT_YAML_PATH'
}
 
/**
 * @description Validate yaml syntactically (syntax correctness)
 *
 * @param {yaml} yaml to validate
 * @returns Promise of list of syntax errors, if any
 *
 */
const validateYAML = (yaml: string): Promise<Diagnostic[]> => {
  /* istanbul ignore next */
  if (!yaml) {
    return Promise.reject('Invalid or empty yaml')
  }
  const textDocument = TextDocument.create('', 'yaml', 0, yaml)
  return yamlLanguageService.getLanguageService()?.doValidation(textDocument, false)
}
 
/**
 * @description Validate yaml semantically (adherence to a schema)
 *
 * @param {yaml} yamlString to validate
 * @param {schema} schema to validate yaml against
 * @returns Promise of list of semantic errors, if any
 *
 */
 
const validateYAMLWithSchema = (yamlString: string, schema: Record<string, any>): Promise<Diagnostic[]> => {
  Iif (!yamlString) {
    return Promise.reject('Invalid or empty yaml.')
  }
  Iif (isEmpty(schema)) {
    return Promise.reject('Invalid or empty schema.')
  }
  const textDocument = TextDocument.create('', 'yaml', 0, yamlString)
  const languageService = setUpLanguageService(schema)
  return languageService?.doValidation(textDocument, false)
}
 
const getPartialYAML = (tokens: string[], endingIndex: number): string => {
  if (isEmpty(tokens) || endingIndex + 1 > tokens.length) {
    return ''
  }
  return tokens.slice(0, endingIndex + 1).join('\n')
}
 
/**
 * @description Validate a JSON against a schema
 *
 * @param jsonObj json to be validated
 * @param schema schema against which json is to be validated
 * @returns Map of json path to list of errors at that path
 */
async function validateJSONWithSchema(
  jsonObj: Record<string, any>,
  schema: Record<string, any>
): Promise<Map<string, string[]>> {
  const errorMap = new Map<string, string[]>()
 
  Iif (isEmpty(jsonObj) || isEmpty(schema)) {
    return errorMap
  }
 
  try {
    const yamlEqOfJSON = yamlStringify(jsonObj)
    const lineContents = yamlEqOfJSON.split(/\r?\n/)
    const validationErrors = await validateYAMLWithSchema(yamlEqOfJSON, getSchemaWithLanguageSettings(schema))
    validationErrors.map(error => {
      const idx = error.range.end.line
      if (idx <= lineContents.length) {
        const key = lineContents[idx]?.split(':')?.[0]?.trim()
        const partialYAML = getPartialYAML(lineContents, idx)
        const partialJSONEqofYAML = parse(partialYAML)
        if (key && !isEmpty(partialJSONEqofYAML)) {
          const jsonPathOfKey = findLeafToParentPath(partialJSONEqofYAML, key)
          if (jsonPathOfKey) {
            if (errorMap.has(jsonPathOfKey)) {
              const existingErrors = errorMap.get(jsonPathOfKey) || []
              existingErrors.push(error.message)
              errorMap.set(jsonPathOfKey, existingErrors)
            } else {
              errorMap.set(jsonPathOfKey, [error.message])
            }
          }
        }
      }
    })
 
    return errorMap
  } catch (err) {
    return errorMap
  }
}
 
const setUpLanguageService = (schema: Record<string, any>) => {
  const languageService = yamlLanguageService.getLanguageService()
  languageService?.configure(schema)
  return languageService
}
 
const getSchemaWithLanguageSettings = (schema: Record<string, any>): Record<string, any> => {
  return {
    validate: true,
    enableSchemaRequest: true,
    hover: true,
    completion: true,
    schemas: [
      {
        fileMatch: ['*'],
        schema
      }
    ]
  }
}
 
export {
  validateYAML,
  validateYAMLWithSchema,
  validateJSONWithSchema,
  getSchemaWithLanguageSettings,
  DEFAULT_YAML_PATH,
  findLeafToParentPath
}