# All files / build-tools massage-spec.ts

35.59% Statements 21/59
22.72% Branches 5/22
45.45% Functions 5/11
37.5% Lines 21/56

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
    1221x
    1x
    1x

    1x
    1x
    1x
    1x

    1x

    1x

    1x
    1x

    1x
    1x
    1x
    1x
    2x
    2x
    1x

    1x

    1x

    1x

     import * as fastJsonPatch from 'fast-json-patch';
    import { schema } from '../lib';
    import { detectScrutinyTypes } from './scrutiny';

    export function massageSpec(spec: schema.Specification) {
      detectScrutinyTypes(spec);
      replaceIncompleteTypes(spec);
      dropTypelessAttributes(spec);
    }

    export function forEachSection(spec: schema.Specification, data: any, cb: (spec: any, fragment: any, path: string[]) => void) {
      cb(spec.PropertyTypes, data.PropertyTypes, ['PropertyTypes']);
      cb(spec.ResourceTypes, data.ResourceTypes, ['ResourceTypes']);
      // Per-resource specs are keyed on ResourceType (singular), but we want it in ResourceTypes (plural)
      cb(spec.ResourceTypes, data.ResourceType, ['ResourceType']);
    }

    export function decorateResourceTypes(data: any) {
      const requiredTransform = data.ResourceSpecificationTransform as string | undefined;
      Iif (!requiredTransform) { return; }
      const resourceTypes = data.ResourceTypes || data.ResourceType;
      for (const name of Object.keys(resourceTypes)) {
        resourceTypes[name].RequiredTransform = requiredTransform;
      }
    }

    /**
     * Fix incomplete type definitions in PropertyTypes
     *
     * Some user-defined types are defined to not have any properties, and not
     * be a collection of other types either. They have no definition at all.
     *
     * Add a property object type with empty properties.
     */
    function replaceIncompleteTypes(spec: schema.Specification) {
      for (const [name, definition] of Object.entries(spec.PropertyTypes)) {
        Iif (!schema.isRecordType(definition)
        && !schema.isCollectionProperty(definition)
        && !schema.isScalarProperty(definition)
        && !schema.isPrimitiveProperty(definition)) {
          // eslint-disable-next-line no-console
          console.log(`[${name}] Incomplete type, adding empty "Properties" field`);

          (definition as unknown as schema.RecordProperty).Properties = {};
        }
      }
    }

    /**
     * Drop Attributes specified with the different ResourceTypes that have
     * no type specified.
     */
    function dropTypelessAttributes(spec: schema.Specification) {
      const resourceTypes = spec.ResourceTypes;
      Object.values(resourceTypes).forEach((resourceType) => {
        const attributes = resourceType.Attributes ?? {};
        Object.keys(attributes).forEach((attrKey) => {
          const attrVal = attributes[attrKey];
          if (Object.keys(attrVal).length === 0) {
            delete attributes[attrKey];
          }
        });
      });
    }

    export function merge(spec: any, fragment: any, jsonPath: string[]) {
      Iif (!fragment) { return; }
      for (const key of Object.keys(fragment)) {
        if (key in spec) {
          const specVal = spec[key];
          const fragVal = fragment[key];
          Iif (typeof specVal !== typeof fragVal) {
            // eslint-disable-next-line max-len
            throw new Error(`Attempted to merge ${JSON.stringify(fragVal)} into incompatible ${JSON.stringify(specVal)} at path ${jsonPath.join('/')}/${key}`);
          }
          Iif (typeof specVal !== 'object') {
            // eslint-disable-next-line max-len
            throw new Error(`Conflict when attempting to merge ${JSON.stringify(fragVal)} into ${JSON.stringify(specVal)} at path ${jsonPath.join('/')}/${key}`);
          }
          merge(specVal, fragVal, [...jsonPath, key]);
        } else {
          spec[key] = fragment[key];
        }
      }
    }

    export function patch(spec: any, fragment: any) {
      Iif (!fragment) { return; }
      if ('patch' in fragment) {
        // eslint-disable-next-line no-console
        console.log(`Applying patch: ${fragment.patch.description}`);
        fastJsonPatch.applyPatch(spec, fragment.patch.operations);
      } else {
        for (const key of Object.keys(fragment)) {
          patch(spec[key], fragment[key]);
        }
      }
    }

    /**
     * Modifies the provided specification so that ``ResourceTypes`` and ``PropertyTypes`` are listed in alphabetical order.
     *
     * @param spec an AWS CloudFormation Resource Specification document.
     *
     * @returns ``spec``, after having sorted the ``ResourceTypes`` and ``PropertyTypes`` sections alphabetically.
     */
    export function normalize(spec: schema.Specification): schema.Specification {
      spec.ResourceTypes = normalizeSection(spec.ResourceTypes);
      Iif (spec.PropertyTypes) {
        spec.PropertyTypes = normalizeSection(spec.PropertyTypes);
      }
      return spec;

      function normalizeSection<T>(section: { [name: string]: T }): { [name: string]: T } {
        const result: { [name: string]: T } = {};
        for (const key of Object.keys(section).sort()) {
          result[key] = section[key];
        }
        return result;
      }
    }