Ah, so you’re back for more, eh? Couldn’t get enough the first time? You’re a glutton for punishment? Or maybe you’re just one of those weirdos who likes to read the end of the book first and skipped Part 1 of this series. If this is you, go get help. In part 2, we’re going to continue working with APIs in PowerShell by transforming our previous script into a function. Conjunction junction…what’s yo func…just imagine me singing this and I’ll imagine you begging me to make it stop.

\\\Disfunction Dis

Okay, so our previous script works and allows us to switch to various endpoints with just a few variables. We tested connecting to our Goodreads account so we could pull book and author info. First, let’s go over a few basic parts of a PowerShell function and the idea of how we’ll use it to make our life easier. If you’re unfamiliar with functions, think of it like wrapping your script or a portion of your script so you can one line that dude again and again. We can get wayyyy off track talking about how amazing advanced functions are and how they can lower your cholesterol and extend your vehicle warranty. But for now, I want to introduce you to a couple of techniques we’ll use specifically in this function.

–Positing the position

For each parameter in our function, we have parameter options that can give us more control over our parameters. One of these is the Position. Without any other settings defined, it would look like this:

[Parameter(Position=0)]
$Parameter}

If you’ve ever taken advantage of the Get-ADUser cmdlet and did something like this:

Get-ADUser lskywalker

Instead of this:

Get-ADUser -Identity lskywalker

This is possible because the -Identity parameter is defined as Position=0. It allows the first value to be assigned to the -Identity parameter even if it wasn’t referenced by name. Very cool. I’m not sure about you, but I like typing less and thereby narrowing the tunnel for carpal tunnel.

–Validate Me

Our next technique is the ValidateSet parameter option and this little beauty will give you more validation than your sweet Aunt Petunia did when you were a kid. Since our previous script included a switch statement, we can easily restrict a parameter to a series of values and use tab completion in our function. This looks like this:

[ValidateSet('User','Author','BooksByAuthor','Book')]
$Parameter

This is helpful if you’re giving this function to somebody else or if you don’t have total recall and you’ve slept since the last time your ran this function.

–Heard It Through the Pipeline

How much longerrrr will you be mine! Oh, sorry. Lastly, we’re going to work that pipeline to our advantage. This is something I didn’t take advantage of until far later in my scripting adventure. It would have really come in handy if I would have learned it sooner. This parameter option looks like this:

[Parameter(ValueFromPipeline=$true)]<br />$Parameter

There’s a lot of information on the various pipelines that PowerShell uses to pass information along, but for now let’s just talk about what this setting does. If you’ve ever piped one cmdlet to another, you’re essentially using the output of one function as the input for another function. Here’s an example from Exchange:

Get-Mailbox | Get-MailboxStatistics | Select TotalItemSize

We’re getting mailboxes using Get-Mailbox, then we’re piping the results to the Get-MailboxStatistics cmdlet before finally piping to the select (alias for Select-Object) to choose the TotalItemSize property. As handy as this ability is, our function can do the same and accept a value from the pipeline for a specific parameter.

\\\How bad is it, doc?

I know it seems like more work, but this function is going to make working with APIs in PowerShell easy peasy. Let’s look at how our function is coming together so far before getting into the nuts and bolts.

function API-Quickie{
    [CmdletBinding()]
    Param(
        # Param1 help description
        [Parameter(Mandatory=$true,Position=0)]
        [ValidateSet('User','Author','BooksByAuthor','Book')]
        $Endpoint,
        # Criteria for the endpoint (usually the id of the object or name to query)
        [Parameter(ValueFromPipeline=$true)]
        $Criteria
    )
    Begin{
        $Key = 'XXXXXXXXXXXXXXXX'
        $KeyString = "key=$Key"
        $BaseURL = 'https://www.goodreads.com/'
    }
    Process{
    }
    End{
    }
}

It’s ugly and doesn’t do much right now, but it’s our ugly and we’re proud of the little critter. In Part 1 we went over the Switch Statement that allowed us to easily switch between endpoints. One part we’re going to add to is for the BooksByAuthor endpoint. I didn’t want to overcomplicate Part 1 but the Goodreads API only returns 30 books at a time. I made a mistake and tested with Steven King and found a whopping 1764 books by The King. Dang. At 30 books a call, we have to make 59 calls to get all of the books! That’s a tough……….call………? Sorry, somebody let me out of the Dad Cave…

\\\Overachieving Authors

So for this next section I want to expound on it and explain what I’m doing. In a nutshell, we have to dynamically determine how many books there are and how many calls need to be made. If the author has less than 30 books, we only need to make one call, preferably not to Liam Neeson. If the author has more than 30 books, we need to determine how many and keep calling like a Karen who ordered the new chicken sandwich from McDonald’s and was sorely disappointed. But not too many times…just enough times to make sure the manager gets the point.

To do this, the Goodreads payload contains author.books.end (last book returned in this call) and author.books.total (total of number of books by the author). We use an empty array so we can keep adding all of our books (30 at a time). We also use a $PageNumber variable that we’ll increment using ++. And finally, we’re utilizing a Do Until Statement so we’ll keep getting results until we reach our last page.

All of that together looks like this:

Get those books
        if($Endpoint -eq 'BooksByAuthor'){
            
            #Get Results
            $Results = (Invoke-RestMethod -Uri $URI -Method Get).GoodreadsResponse
            #This endpoint only returns 30 books at a time,
            #so the "end" is the last book returned in this call,
            #and "total" is the total number of books that Goodreads has for this author
            #if there is last book returned doesn't equal the total, we want to call it again
            if($Results.author.books.end -ne $Results.author.books.total){
                #Our first call is technically Page 1, even without the page parameter defined
                $PageNumber = 2
                $CombinedResults = @()
                $CombinedResults += $Results.author.books.book
                #Now, let's add the page parameter and call again, adding the $Results to the $CombinedResults array
                #and then increment the $PageNumber by 1. Do all of this until the last book returned
                #is the same number as the total number of books in Goodreads for this author
                Do{
                    $URI += "&page=$PageNumber"
                    $Results = (Invoke-RestMethod -Uri $URI -Method Get).GoodreadsResponse
                    $CombinedResults += $Results.author.books.book
                    $PageNumber ++
                }
                Until($Results.author.books.end -eq $Results.author.books.total)
            #Finall, return the combined results of all of the books
            $CombinedResults
            }
            #this else is used if the author has less than 30 books
            else{
                $Results.author.books.book
            }
        }
        #else (if the endpoint isn't 'BooksByAuthor')
        else{
            $Results = Invoke-RestMethod -Uri $URI -Method Get
            $Results.GoodreadsResponse
        }

To help visualize all of the if’s and else’s going on here, I’ve created this hand-drawn diagram on papyrus for you:

Final Destination

Finally, fate has brought us to our conclusion and we can look back at our work with great fondness. Here’s the finished function:

Grand Finale
<#
.Synopsis
   Reading is good. Enter: Goodreads (bum bum bum!)
.DESCRIPTION
   Probably the most amazing function we've ever seen. Super useful and something we'll remember for the rest of our lives. You can quickly use the Goodreads API to get user, author, books, or books by author.
.ENDPOINT
   Specific Endpoint to call with the Goodreads API.
   Choose between:
   [User]  [Author]  [BooksByAuthor]  [Book]
.CRITERIA
   This is the search string you're wanting to look up. Could be the author's name, title of book, etc.
.EXAMPLE
    Get information about author named HG Wells:
    
    API-Quickie -Endpoint Author -Criteria 'HG Wells'
.EXAMPLE
    Get information about an author and then pipe the author ID to get books by the author. Notice we can pipe to the Criteria parameter and the Endpoint is positioned at 0 so if it's our first parameter we don't have to define it with -Endpoint
    
    (API-Quickie author -Criteria 'Ernest Cline').author.id | API-Quickie booksbyauthor
#>
function API-Quickie{
    [CmdletBinding()]
    Param(
        # API Endpoint
        [Parameter(Mandatory=$true,Position=0)]
        [ValidateSet('User','Author','BooksByAuthor','Book')]
        $Endpoint,
        # Criteria for the endpoint (usually the id of the object or name to query)
        [Parameter(ValueFromPipeline=$true)]
        $Criteria
    )
    Begin{
        $Key = 'XXXXXXXXXXXXX'
        $KeyString = "key=$Key"
        $BaseURL = 'https://www.goodreads.com/'
    }
    Process{
        Switch ($Endpoint){
            'User'{
                $EndpointString = "user/show/$Criteria.xml?$KeyString"
                $URI = $BaseURL + $EndpointString
                #https://www.goodreads.com/user/show/8898794.xml?key=3ZjCCwF94iXQHUzceAdkA
            }
            'Author'{
                #Search author by name
                $EndpointString = 'api/author_url/'
                $URI = $BaseURL + $EndpointString + $Criteria + "/?$KeyString"
                #https://www.goodreads.com/api/author_url/Orson%20Scott%20Card?key=3ZjCCwF94iXQHUzceAdkA
            }
            'BooksByAuthor'{
                #Books by Author using Author's ID
                $EndpointString = "author/list/$Criteria" + "?format=xml&$KeyString"
                $URI = $BaseURL + $EndpointString
                #https://www.goodreads.com/author/list/18541?format=xml&key=3ZjCCwF94iXQHUzceAdkA
            }
            'Book'{
                #Book Info using Book's ID
                $EndpointString = "book/show/$Criteria.xml?$KeyString"
                $URI = $BaseURL + $EndpointString
                #https://www.goodreads.com/book/show/50.xml?key=3ZjCCwF94iXQHUzceAdkA
            }
        }
        if($Endpoint -eq 'BooksByAuthor'){
            
            #Get Results
            $Results = (Invoke-RestMethod -Uri $URI -Method Get).GoodreadsResponse
            #This endpoint only returns 30 books at a time,
            #so the "end" is the last book returned in this call,
            #and "total" is the total number of books that Goodreads has for this author
            #if there is last book returned doesn't equal the total, we want to call it again
            if($Results.author.books.end -ne $Results.author.books.total){
                #Our first call is technically Page 1, even without the page parameter defined
                $PageNumber = 2
                $CombinedResults = @()
                $CombinedResults += $Results.author.books.book
                #Now, let's add the page parameter and call again, adding the $Results to the $CombinedResults array
                #and then increment the $PageNumber by 1. Do all of this until the last book returned
                #is the same number as the total number of books in Goodreads for this author
                Do{
                    $URI += "&page=$PageNumber"
                    $Results = (Invoke-RestMethod -Uri $URI -Method Get).GoodreadsResponse
                    $CombinedResults += $Results.author.books.book
                    $PageNumber ++
                }
                Until($Results.author.books.end -eq $Results.author.books.total)
            #Finall, return the combined results of all of the books
            $CombinedResults
            }
            #this else is used if the author has less than 30 books
            else{
                $Results.author.books.book
            }
        }
        #else (if the endpoint isn't 'BooksByAuthor')
        else{
            $Results = Invoke-RestMethod -Uri $URI -Method Get
            $Results.GoodreadsResponse
        }
    }
    End{
    }
}

So what good is writing a function without making sure it can function? Let’s look at why we did all of that fancy parameter fun in the beginning of this post. Since our $Endpoint parameter has Position 0, we can go ahead and save ourselves the agonizing pain of typing 9 extra characters. And since our $Criteria parameter accepts a value from the pipeline, we can live out our pipedream and just pipe everything everywhere like the Rowdy Roddy Piper.

Here’s an example of how we could search an author by name, and then use the Author ID and pipe it to search for books by that author.

(API-Quickie Author -Criteria 'Ernest Cline').author.id | API-Quickie BooksByAuthor

And now you’ve gone from just working with APIs in PowerShell to a-whole-nother dimension with your own function. Can you feel that power in your shell?

OTHER POSTS YOU WANT TO READ

Index Scripts for Windows Search

So you just finished writing some code and you go to save your file. You summarize all of the important aspects this section of code contains into a nice, easy-to-read file name that your future self will immediately recognize. Fast forward to the future where you...

Array vs ArrayList (PowerShell)

For some tasks in life, being precise is a necessity. But most of us get away with rounding, paraphrasing, and hitting in the general vicinity most of the time. Depending on your personality, you may be one who strives for perfection and strains on every miniscule...

Spice Up HTML Emails with PowerShell – Part III

So far in this series we've fumbled our way around the kitchen and tried to wing it when sending HTML emails with PowerShell. It was clunky to say the least. We then went through our spice rack and built an HTML template, highlighting the nuances of each spicy element...

Spice up HTML Emails with PowerShell – Part II

In Part I of our scrumptious concoction we put our script into the oven to let it bake. But we forgot to add our secret sauce that's sure to leave our recipients drooling, which is clearly our goal. In this post we'll continue to spice up HTML emails with PowerShell...

Spice Up HTML Emails with PowerShell – Part I

I live in the South, specifically in God's country AKA TEXAS BABAY! There's plenty of amazing things about Texas and definitely some reasons to loathe being from Houston, but it's hard to knock our food. I mean, is there really even a debate between Whataburger vs...
%d bloggers like this: