VSTS.psm1

# System.Web is not always loaded by default, so ensure it is loaded.
Add-Type -AssemblyName System.Web

# Import all the lib files
$moduleRoot = Split-Path `
    -Path $MyInvocation.MyCommand.Path `
    -Parent

$libs = Get-ChildItem `
    -Path (Join-Path -Path $moduleRoot -ChildPath 'lib') `
    -Include '*.ps1' `
    -Recurse
$libs.Foreach(
    {
        Write-Verbose -Message ('Importing the lib file {0}' -f $_.Fullname)
        . $_.Fullname
    }
)

<#
    .SYNOPSIS
    Create a new VSTS session object that needs to be passed
    to other VSTS module calls to provide connection
    information. It can be used to connect to VSTS or TFS
    APIs.

    .PARAMETER AccountName
    The name of the VSTS account to use. Not required for TFS
    sessions.

    .PARAMETER User
    This user name to authenticate to VSTS or TFS.

    .PARAMETER Token
    This personal access token to use to authenticate to VSTS
    or TFS.

    .PARAMETER Collection
    This collection to use. This defaults to
    'DefaultCollection'.

    .PARAMETER Server
    The name of the VSTS or TFS Server to connect to.
    For VSTS this will be 'visualstudio.com', but for TFS
    this should be set to the TFS server. The default value
    if this is not specified is 'visualstudio.com'.

    .PARAMETER HTTPS
    Use HTTP or HTTPS to connect to the server.
    Defaults to HTTPS.

    .OUTPUTS
    VSTS Session Object.
#>

function New-VstsSession
{
    [CmdletBinding(DefaultParameterSetName = 'VSTS')]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(Mandatory = $true, ParameterSetName = 'VSTS')]
        [ValidateNotNullOrEmpty()]
        [String] $AccountName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $User,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Token,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [String] $Collection = 'DefaultCollection',

        [Parameter(Mandatory = $true, ParameterSetName = 'TFS')]
        [ValidateNotNullOrEmpty()]
        [String] $Server,

        [Parameter()]
        [ValidateSet('HTTP', 'HTTPS')]
        [String] $Scheme = 'HTTPS'
    )

    if ($PSCmdlet.ParameterSetName -eq 'VSTS')
    {
        $Server = 'visualstudio.com'
    }

    [PSCustomObject] @{
        AccountName = $AccountName
        User        = $User
        Token       = $Token
        Collection  = $Collection
        Server      = $Server
        Scheme      = $Scheme
    }
}

<#
    .SYNOPSIS
    Helper function that takes an array of bound
    parameters passed to the calling function
    and an array of parameter names and creates a hash
    table containing each parameter that appears in
    the Bound Parameters and in the Parameters
    List.

    .PARAMETER BoundParameters
    This is the content of the PSBoundParameters from
    the calling function.

    .PARAMETER ParameterList
    This is the list of parameters to extract from the
    bound parameters list.

    .OUTPUTS
    Hashtable containing all parameters from
    BoundParameters that also appear in ParameterList.
#>

function Get-VstsQueryStringParametersFromBound
{
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        $BoundParameters,

        [Parameter(Mandatory = $true)]
        [Array] $ParameterList
    )

    $result = @{}
    foreach ($parameter in $ParameterList)
    {
        if ($BoundParameters.ContainsKey($parameter))
        {
            $result += @{ $parameter = $BoundParameters[$parameter] }
        }
    }
    return $result
}

<#
    .SYNOPSIS
    Assembles a VSTS or TFS endpoint URI object
    to be used to connect to a VSTS or TFS endpoint.

    .PARAMETER Session
    The session object created by New-VstsSession.

    .PARAMETER EndpointName
    Set an alternate VSTS endpoint to call.
    This is required by API calls for to preview APIs that are not
    yet available on the primary endpoint.
#>

function Get-VstsEndpointUri
{
    [CmdletBinding()]
    [OutputType([System.UriBuilder])]
    param
    (
        [Parameter(Mandatory = $true)]
        $Session,

        [Parameter()]
        [String] $EndpointName
    )

    if ([String]::IsNullOrEmpty($Session.AccountName))
    {
        $argumentList = ('{0}://{1}' -f $Session.Scheme, $Session.Server)
    }
    else
    {
        if ([String]::IsNullOrEmpty($EndpointName))
        {
            $argumentList = ('{0}://{1}.visualstudio.com' -f $Session.Scheme, $Session.AccountName)
        }
        else
        {
            $argumentList = ('{0}://{1}.{2}.visualstudio.com' -f $Session.Scheme, $Session.AccountName, $EndpointName)
        }
    }

    $uriBuilder = New-Object `
        -TypeName System.UriBuilder `
        -ArgumentList $argumentList

    return $uriBuilder
}

<#
    .SYNOPSIS
    Invokes the VSTS REST API endpoint.

    .PARAMETER Session
    The session object created by New-VstsSession.

    .PARAMETER QueryStringParameters
    A hash table containing any additional query string
    parameters to add to the URI.

    .PARAMETER Project
    The name of the project to invoke the REST API for.

    .PARAMETER Path
    The path to add to the URI.

    .PARAMETER ApiVersion
    The version of the REST API to use.

    .PARAMETER Method
    The method to use for the REST API. Deraults to 'GET'.

    .PARAMETER Body
    The body to pass in the REST call.

    .PARAMETER EndpointName
    Set an alternate VSTS endpoint to call.
    This is required by API calls for to preview APIs that are not
    yet available on the primary endpoint.

    .PARAMETER QueryStringExtParameters
    A hash table containing any additional query string
    parameters to add to the URI. These will be added with a '$'
    pre-pended to the query string name. E.g. '&$Top=10'.
#>

function Invoke-VstsEndpoint
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        $Session,

        [Parameter()]
        [Hashtable] $QueryStringParameters,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [String] $Project,

        [Parameter()]
        [Uri] $Path,

        [Parameter()]
        [String] $ApiVersion = '1.0',

        [ValidateSet('GET', 'PUT', 'POST', 'DELETE', 'PATCH')]
        [String] $Method = 'GET',

        [Parameter()]
        [String] $Body,

        [Parameter()]
        [String] $EndpointName,

        [Parameter()]
        [Hashtable] $QueryStringExtParameters
    )

    $queryString = [System.Web.HttpUtility]::ParseQueryString([string]::Empty)

    if ($QueryStringParameters -ne $null)
    {
        foreach ($parameter in $QueryStringParameters.GetEnumerator())
        {
            $queryString[$parameter.Key] = $parameter.Value
        }
    }

    <#
        These are query parmaeters that will be added prepended with a $.
        They can't be passed in the QueryStringParameters.
    #>

    if ($QueryStringExtParameters -ne $null)
    {
        foreach ($parameter in $QueryStringExtParameters.GetEnumerator())
        {
            $queryString['$' + $parameter.Key] = $parameter.Value
        }
    }

    $queryString["api-version"] = $ApiVersion
    $queryString = $queryString.ToString()

    $authorization = Get-VstsAuthorization -User $Session.User -Token $Session.Token

    $collection = $Session.Collection

    $uriBuilder = Get-VstsEndpointUri -Session $Session -EndpointName $EndpointName
    $uriBuilder.Query = $queryString

    if ([String]::IsNullOrEmpty($Project))
    {
        $uriBuilder.Path = ('{0}/_apis/{1}' -f $collection, $Path)
    }
    else
    {
        $uriBuilder.Path = ('{0}/{1}/_apis/{2}' -f $collection, $Project, $Path)
    }

    $uri = $uriBuilder.Uri

    Write-Verbose -Message "Invoke URI [$uri]"

    $contentType = 'application/json'
    $invokeRestMethodParameters = @{
        Uri         = $Uri
        Method      = $Method
        ContentType = $ContentType
        Headers     = @{ Authorization = $authorization }
    }

    if ($Method -eq 'PUT' -or $Method -eq 'POST' -or $Method -eq 'PATCH')
    {
        if ($Method -eq 'PATCH')
        {
            $invokeRestMethodParameters['contentType'] = 'application/json-patch+json'
        }

        $invokeRestMethodParameters += @{
            Body = $Body
        }
    }

    $restResult = Invoke-RestMethod @invokeRestMethodParameters

    if ($restResult.Value)
    {
        return $restResult
    }
    else
    {
        <#
            A Value property wasn't returned which usually occurs
            if a specific record is requested from the API.
            So create a new object with the value property set
            to the returned object.
        #>

        return [psobject] @{
            Value = $restResult
        }
    }
}

<#
    .SYNOPSIS
    Generates a VSTS authorization header value from a username and Personal
    Access Token.

    .PARAMETER User
    The username of the account to generate the authentication header for.

    .PARAMETER Token
    The Personal Access Token to use in the authentication header.
#>

function Get-VstsAuthorization
{
    [CmdletBinding()]
    [OutputType([String])]
    param
    (
        [Parameter(Mandatory = $True)]
        [String] $User,

        [Parameter(Mandatory = $True)]
        [String] $Token
    )

    $value = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $User, $Token)))
    return ("Basic {0}" -f $value)
}

<#
    .SYNOPSIS
    Checks that a Guid is valid.

    .PARAMETER Guid
    The Guid to validate.

    .OUTPUTS
    Returns true if the Guid is valid.
#>

function Test-Guid
{
    [CmdletBinding()]
    [OutputType([Boolean])]
    param
    (
        [Parameter(Mandatory = $True)]
        [String] $Guid
    )

    $newGuid = [Guid]::Empty
    [Guid]::TryParse($Guid, [ref]$newGuid)
}