src/metadata/GraphBuilder.ps1

# Copyright 2018, Adam Edwards
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

. (import-script GraphDataModel)
. (import-script EntityEdge)
. (import-script EntityVertex)
. (import-script EntityGraph)

ScriptClass GraphBuilder {

    $graphEndpoint = $null
    $version = $null
    $dataModel = $null
    $namespace = $null
    $percentComplete = 0
    $metadata = $null
    $deferredBuild = $false

    function __initialize($graphEndpoint, $version, $metadata, $deferredBuild) {
        $this.graphEndpoint = $graphEndpoint
        $this.version = $version
        $this.metadata = $metadata
        $this.dataModel = new-so GraphDataModel $metadata
        $this.namespace = $this.dataModel |=> GetNamespace
        $this.deferredBuild = $deferredBuild
    }

    function NewGraph {
        $graph = new-so EntityGraph $this.namespace $this.version $this.graphEndpoint

        __UpdateProgress 0

        __AddRootVertices $graph

        __AddEntitytypeVertices $graph

        __AddEdgesToEntityTypeVertices $graph

        __ConnectEntityTypesWithMethodEdges $graph

        __CopyEntityTypeEdgesToSingletons $graph

        __UpdateProgress 100

        $graph
    }

    function __AddRootVertices($graph) {
        $singletons = $this.dataModel |=> GetSingletons
        __AddVerticesFromSchemas $graph $singletons

        $entitySets = $this.dataModel |=> GetEntitySets
        __AddVerticesFromSchemas $graph $entitySets

        __UpdateProgress 5
    }

    function __AddVerticesFromSchemas($graph, $schemas) {
        $progressTotal = $schemas.count
        $progressIndex = 0

        $schemas | foreach {
            $::.ProgressWriter |=> WriteProgress -id 2 -activity "Adding $($_.localname) vertices" -Status "In progress" -PercentComplete (100 * ($progressIndex / $progressTotal)) -currentoperation "Adding $($_.name)"
            __AddVertex $graph $_
            $progressIndex += 1
        }
    }

    function __AddVertex($graph, $schema) {
        $entity = new-so Entity $schema $this.namespace
        $graph |=> AddVertex $entity
    }

    function __AddEntityTypeVertices($graph) {
        $entityTypes = $this.dataModel |=> GetEntityTypes
        __AddVerticesFromSchemas $graph $entityTypes

        __UpdateProgress 20
    }

    function __AddEdgesToEntityTypeVertices($graph) {
        $types = $graph.typeVertices.Values
        $progressTotal = $types.count
        $progressIndex = 0

        $types | foreach {
            $source = $_
            $::.ProgressWriter |=> WriteProgress -id 2 -activity "Adding entity type navigations" -currentoperation "Processing entity $($source.name)" -percentcomplete (100 * ( $progressIndex / $progressTotal ))
            $transitions = if ( $source.entity.navigations ) {
                $source.entity.navigations
            } else {
                @()
            }
            $transitions | foreach {
                $transition = $_
                $sink = $graph |=> TypeVertexFromTypeName $transition.typedata.entitytypename
                if ( $sink -ne $null ) {
                    $edge = new-so EntityEdge $source $sink $transition
                    $source |=> AddEdge $edge
                } else {
                    write-verbose "Unable to find entity type for '$($transition.type)', skipping"
                }
            }
            $progressIndex += 1
        }
        __UpdateProgress 40
    }

    function __ConnectEntityTypesWithMethodEdges($graph) {
        $actions = $this.dataModel |=> GetActions
        __AddMethodTransitions $graph $actions

        $functions = $this.dataModel |=> GetFunctions
        __AddMethodTransitions $graph $functions

        __UpdateProgress 75
    }

    function __CopyEntityTypeEdgesToSingletons($graph) {
        if ( ! $this.deferredBuild ) {
            $this.scriptclass |=> __CopyEntityTypeEdgesToSingletons $graph
        } else {
            write-verbose "Deferred build set -- skipping connection of singletons to entity types to avoid deserialization depth issues"
        }
    }

    function __UpdateProgress($deltaPercent) {
        $metadataActivity = "Building graph version '$($this.version)' for endpoint '$($this.graphEndpoint)'"

        $this.percentComplete += $deltaPercent
        $completionArguments = if ( $this.percentComplete -ge 100 ) {
            @{Status="Complete";PercentComplete=100;Completed=[System.Management.Automation.SwitchParameter]::new($true)}
        } else {
            @{Status="In progress";PercentComplete=$this.percentComplete}
        }
        $::.ProgressWriter |=> WriteProgress -id 1 -activity $metadataActivity @completionArguments
    }

    function __AddMethodTransitions($graph, $methods) {
        $methods | foreach {
            $parameters = try {
                $_.parameter
            } catch {
            }

            $method = $_
            $source = if ( $parameters ) {
                $bindingParameter = $parameters | where { $_.name -eq 'bindingParameter' -or $_.name -eq 'bindParameter' }
                if ( $bindingParameter ) {
                    $bindingTargetVertex = $graph |=> TypeVertexFromTypeName $bindingParameter.Type

                    if ( $bindingTargetVertex ) {
                        $bindingTargetVertex
                    } else {
                        write-verbose "Unable to bind '$($_.name)' of type '$($bindingParameter.Type)', skipping"
                    }
                } else {
                    write-verbose "Unable to find a bindingParameter in parameters for $($_.name)"
                }
            } else {
                write-verbose "Method '$($_.name)' does not have a parameter attribute, skipping"
            }

            if ( $source ) {
                $sink = if ( $method | gm ReturnType ) {
                    $typeName = if ( $method.localname -eq 'function' ) {
                        $method.ReturnType.Type
                    } else {
                        $method.ReturnType
                    }

                    $typeVertex = $graph |=> TypeVertexFromTypeName $typeName

                    if ( $typeVertex ) {
                        $typeVertex
                    } else {
                        write-verbose "Type $($typeName) returned by $($method.name) cannot be found, configuring Scalar vertex"
                        $::.EntityVertex.ScalarVertex
                    }
                } else {
                    $::.Entityvertex.NullVertex
                }

                __AddMethod $source $method $sink
            }
        }
    }

    function __AddMethod($targetVertex, $methodSchema, $returnTypeVertex) {
        if ( ! ($targetVertex |=> EdgeExists($methodSchema.name)) ) {
            $methodEntity = new-so Entity $methodSchema $this.namespace
            $edge = new-so EntityEdge $targetVertex $returnTypeVertex $methodEntity
            $targetVertex |=> AddEdge $edge
        } else {
            write-verbose "Skipped add of edge $($methodSchema.name) to $($returnTypeVertex.id) from vertex $($targetVertex.id) because it already exists."
        }
    }

    static {
        function CompleteDeferredBuild($graph) {
            write-verbose "Completing deferred build by connecting singletons"
            __CopyEntityTypeEdgesToSingletons $graph
        }

        function __CopyEntityTypeEdgesToSingletons($graph) {
            ($graph |=> GetRootVertices).values | foreach {
                $source = $_
                $edges = if ( $source.type -eq 'Singleton' ) {
                    $typeVertex = $graph |=> TypeVertexFromTypeName ($source.entity.typeData).EntityTypeName
                    if ( $typeVertex -eq $null ) {
                        throw "Unable to find an entity type for type '$($source.entity.type)"
                    }
                    $typeVertex.outgoingEdges.values | foreach {
                        if ( ( $_ | gm transition ) -ne $null ) {
                            $_
                        }
                    }
                }

                $edges | foreach {
                    $sink = $_.sink
                    $transition = $_.transition
                    $edge = new-so EntityEdge $source $sink $transition
                    $source |=> AddEdge $edge
                }
            }
        }
    }
}