function filterModel()

in compiler/src/transform/filter-by-availability.ts [26:252]


function filterModel (inputModel: Model, stack: boolean, serverless: boolean, visibility: string[]): Model {
  // filter over visibility only exclude if present and not matching, include by default.
  function includeVisibility (localVis: Visibility | undefined): boolean {
    if (localVis === undefined || visibility === undefined) {
      return true
    }
    return visibility.includes(localVis)
  }

  // filter used against the provided availability
  // used to filter out endpoints and as a filter for items with availability (Enum & Property).
  function include (availabilities: Availabilities): boolean {
    if ((availabilities.stack !== undefined) && stack) {
      return includeVisibility(availabilities.stack.visibility)
    }
    if ((availabilities.serverless !== undefined) && serverless) {
      return includeVisibility(availabilities.serverless.visibility)
    }

    return false
  }

  // used to filter out individual items within types.
  function filterItem () {
    return (item) => {
      return (item.availability !== undefined) ? include(item.availability) : true
    }
  }

  // short comparison for two TypeNames
  function cmpTypeNames (t1, t2: TypeName): boolean {
    return t1.name === t2.name && t1.namespace === t2.namespace
  }

  // Returns the fully-qualified name of a type name
  function fqn (name: TypeName): string {
    return `${name.namespace}:${name.name}`
  }

  // return early if the type already has been added
  // fetches the original type from the input model
  // save its presence to prevent recursion and doubles
  // continues down the type tree for any new types
  function addTypeToOutput (typeName: TypeName): void {
    if (seen.has(fqn(typeName))) {
      return
    }

    inputModel.types.forEach((typeDef) => {
      if (cmpTypeNames(typeName, typeDef.name)) {
        // add the TypeDefinition to the output
        output.types.push(typeDef)

        // store the type infos to prevent doubles & recursive calls
        seen.add(fqn(typeName))

        // first time seeing this type so we explore the type
        exploreTypedef(typeDef)
      }
    })
  }

  // handles the basic type field we can find
  // user_defined_value and literal_value are omitted.
  function addValueOf (item: ValueOf): void {
    switch (item.kind) {
      case 'instance_of':
        addTypeToOutput(item.type)
        break
      case 'array_of':
        addValueOf(item.value)
        break
      case 'union_of':
        item.items.forEach((member) => {
          addValueOf(member)
        })
        break
      case 'dictionary_of':
        addValueOf(item.key)
        addValueOf(item.value)
        break
    }
  }

  function exploreTypedef (typeDef: TypeDefinition): void {
    // handle generics
    // not really useful for everyone, still useful for type_alias
    if (typeDef.kind === 'interface' || typeDef.kind === 'request' || typeDef.kind === 'response' || typeDef.kind === 'type_alias') {
      typeDef.generics?.forEach((generic) => {
        addTypeToOutput(generic)
      })
    }

    // handle behaviors
    if (typeDef.kind === 'interface' || typeDef.kind === 'request' || typeDef.kind === 'response') {
      typeDef.behaviors?.forEach((behavior) => {
        addTypeToOutput(behavior.type)
        behavior.generics?.forEach((generic) => {
          addValueOf(generic)
        })
      })
    }

    // handle inherits & implements
    if (typeDef.kind === 'interface' || typeDef.kind === 'request') {
      if (typeDef.inherits !== undefined) {
        addTypeToOutput(typeDef.inherits.type)
      }
    }

    // handle body value and body properties for request and response
    if (typeDef.kind === 'request' || typeDef.kind === 'response') {
      switch (typeDef.body.kind) {
        case 'value':
          addValueOf(typeDef.body.value)
          break
        case 'properties':
          typeDef.body.properties.forEach((property) => {
            addValueOf(property.type)
          })
          break
      }
    }

    // left over specific cases
    switch (typeDef.kind) {
      case 'interface':
        typeDef.properties.forEach((property) => {
          addValueOf(property.type)
        })
        break

      case 'request':
        typeDef.path.forEach((path) => {
          addValueOf(path.type)
        })
        typeDef.query.forEach((query) => {
          addValueOf(query.type)
        })
        break

      case 'type_alias':
        addValueOf(typeDef.type)
        break
    }
  }

  const seen = new Set<string>()

  const output: Model = {
    _info: inputModel._info,
    types: new Array<TypeDefinition>(),
    endpoints: new Array<Endpoint>()
  }

  const typeDefByName = new Map<string, TypeDefinition>()

  for (const type of inputModel.types) {
    typeDefByName.set(fqn(type.name), type)
  }

  // we filter to include only the matching endpoints
  inputModel.endpoints.forEach((endpoint) => {
    if (include(endpoint.availability)) {
      // add the current endpoint
      output.endpoints.push(endpoint)

      if (endpoint.request !== null) {
        const requestType = typeDefByName.get(fqn(endpoint.request))
        if (requestType !== undefined) output.types.push(requestType)
      }

      if (endpoint.response !== null) {
        const responseType = typeDefByName.get(fqn(endpoint.response))
        if (responseType !== undefined) output.types.push(responseType)
      }
    }
  })

  // filter type items (properties / enum members)
  inputModel.types.forEach((typeDef) => {
    switch (typeDef.kind) {
      case 'interface':
        typeDef.properties = typeDef.properties.filter(filterItem())
        break
      case 'enum':
        typeDef.members = typeDef.members.filter(filterItem())
        addTypeToOutput(typeDef.name)
        break
      case 'request':
        output.endpoints.forEach((endpoint) => {
          if (endpoint.request?.name === typeDef.name.name && endpoint.request.namespace === typeDef.name.namespace) {
            typeDef.path = typeDef.path.filter(filterItem())
            typeDef.query = typeDef.query.filter(filterItem())
            // filter out body properties
            switch (typeDef.body.kind) {
              case 'properties':
                typeDef.body.properties = typeDef.body.properties.filter(filterItem())
                break
            }
          }
        })
        break
      case 'response':
        output.endpoints.forEach((endpoint) => {
          if (endpoint.response?.name === typeDef.name.name && endpoint.response.namespace === typeDef.name.namespace) {
            // filter out body properties
            switch (typeDef.body.kind) {
              case 'properties':
                typeDef.body.properties = typeDef.body.properties.filter(filterItem())
                break
            }
          }
        })
        break
      case 'type_alias':
        addTypeToOutput(typeDef.name)
    }
  })

  // we complete the spec with the missing types for the tree until exhaustion
  output.types.forEach((typeDef) => {
    exploreTypedef(typeDef)
  })

  return output
}