|
-
Jun 16th, 2017, 11:51 AM
#9
Re: Help using JSON API that requires Authentication with JWT token
We know how to convert objects to and from JSON. We know how to send HTTP POST requests. We've written the /login endpoint for the API. We can get a JWT. Now we have to write code to work with an endpoint that uses it.
I want to write the code for /search/series. It seems relatively easy, and its response object isn't very complicated. I'll have to write a little bit more code to support this at each layer. But first let's talk about the endpoint.
It is a GET function that takes a name, imdbId, or zap2itId and, optionally, a language. It will search for a series using these parameters and return results in the indicated language, if they exist. It returns status 404 if the search turns up no results. For simplicity, I'm only going to implement the 'name' parameter.
We don't have code to send a GET request yet. They behave a little different from POST requests. You don't send a message body along with these, so if they take any parameters they are specified via the URL, and you need to perform 'URL encoding' on them. Also, this request has to take a JWT. So we've got our work cut out for us.
First, let's make some code to help build a URL with parameters. Newer HTTP libraries in .NET do this for you, but HttpWebRequest offers no help. A URL with GET parameters looks like this:
Code:
http://example.com?firstParam=asdf&secondParam=hello%20world
That %20? That stands for a space. There are a lot of characters not valid in URLs, and they have special codes for representation. The URI class has EscapeUriString() and EscapeDataString() to help, but you have to be careful which one you use. Instead of spending a day explaining, I'm just going to show you the right way. Add this to RestClient.vb:
Code:
Private Function CreateEncodedParameterizedUrl(ByVal baseUrl As String, ByVal parameters As Dictionary(Of String, String)) As String
Dim urlBuilder As New Text.StringBuilder()
urlBuilder.Append(baseUrl)
Dim parameterIndex As Integer = 0
urlBuilder.Append("?")
For i As Integer = 0 To parameters.Count() - 1
Dim key As String = parameters.Keys(i)
Dim value As String = parameters(key)
Dim encodedValue As String = Uri.EscapeDataString(value)
urlBuilder.AppendFormat("{0}={1}", key, encodedValue)
If i < parameters.Count - 1 Then
urlBuilder.Append("&")
End If
Next
Return urlBuilder.ToString()
End Function
It's a little bit of a mess. This is the kind of code I like to make MS write for me. There's some newer types that do, but alas, I've already started down the road using HttpWebRequest. The rules are that the first parameter is of the format "?name=value". Every parameter after that is "&name=value". Technically every name and value should be encoded, but I've never seen someone use a parameter name that had special characters. So I'm just encoding the values. Why don't I build the whole thing, then encode it? Because the "://" after "http" would get encoded too! I told you it was tricky.
Anyway, if you look at that code you'll see it one-by-one appends the right start character, then each name/value pair with the value encoded. Now we can build a GET request that uses a JWT. It reuses some code from POST, for the sake of brevity I'll live with duplication. Add this to RestClient.vb:
Code:
Public Function GetWithJwt(ByVal url As String, ByVal jwt As String, Optional ByVal parameters As Dictionary(Of String, String) = Nothing) As String
Dim encodedUrl As String = url
If parameters IsNot Nothing Then
encodedUrl = CreateEncodedParameterizedUrl(url, parameters)
End If
' Configure the request headers.
Dim request As HttpWebRequest = CType(HttpWebRequest.Create(url), HttpWebRequest)
request.Method = "GET"
' Build the JWT string and add the right header.
Dim jwtHeaderValue As String = String.Format("Bearer {0}", jwt)
request.Headers.Add(HttpRequestHeader.Authorization, jwtHeaderValue)
' Read the response and its JSON payload.
Using response As HttpWebResponse = CType(request.GetResponse(), HttpWebResponse)
If response.StatusCode <> HttpStatusCode.OK Then
' This needs to be more sophisticated, you can flesh it out later.
Throw New ApplicationException("There was an error with the request.")
End If
Using responseStream As Stream = response.GetResponseStream()
Using reader As New StreamReader(responseStream, Text.Encoding.UTF8)
Return reader.ReadToEnd()
End Using
End Using
End Using
End Function
Let's pay attention to how it compared to the Rest() method already implemented.
First, there's extra work to decide if the URL needs encoded parameters. I made the parameters optional because not every GET request requires parameters. The next big change is fewer headers need to be configured: the method is set to GET, and Content-Encoding isn't needed.
The next 2 lines are how I think the JWT is added. It's very simple, just building a string and adding a header.
Everything past that is identical: the request is sent, the response is returned.
Now we have the ability to send an HTTP GET method with URL parameters and a JWT. That's what we needed to implement /search/series! I don't want to have to think about HTTP when calling that method, so I'm thinking it needs to look like this:
Code:
Public Function SearchSeriesByName(ByVal jwt As String, ByVal searchTerm As String) As SearchSeriesResult
Ah. Right. I forgot something. I need an object that represents the search result. That JSON is defined in the documentation. This object should do the trick:
Code:
Public Class SearchSeriesResult
Public Property aliases() As String
Public Property banner As String
Public Property firstAired As String
Public Property id As Long
Public Property network As String
Public Property overview As String
Public Property seriesName As String
Public Property status As String
End Class
Now I write the relevant code in the TvDbApi class:
Code:
Imports Newtonsoft.Json
Public Class TvDbApi
Public Function Login(ByVal credentials As TvDbCredentials) As String
...
End Function
Public Function SearchSeriesByName(ByVal jwt As String, ByVal searchTerm As String) As SearchSeriesResult
Dim restClient As New RestClient()
Dim url As String = "https://example.com/search/series"
Dim parameters As New Dictionary(Of String, String)()
parameters.Add("name", searchTerm)
Dim responseJson As String = restClient.GetWithJwt(url, jwt, parameters)
Dim responseObject As SearchSeriesResult = JsonConvert.DeserializeObject(Of SearchSeriesResult)(responseJson)
Return responseObject
End Function
End Class
Calling this should be relatively easy from the context of the main program:
Code:
Dim credentials As New TvDbCredentials()
credentials.apikey = "apikey asdf"
credentials.username = "username asdf"
credentials.userkey = "userkey asdf"
Dim tvDbApi As New TvDbApi()
Dim jwt As String = tvDbApi.Login(credentials)
Dim searchResults As SearchSeriesResult = tvDbApi.SearchSeriesByName(jwt, "Twin Peaks")
Console.WriteLine("First aired: {0}", searchResults.firstAired)
No muss, no fuss. We already know how to get the JWT, so it just gets included with this next call. See how minor a detail the JWT actually is?
Now you're on your own to implement the rest of this API. You might notice I didn't write a way to include a JWT with Post(). That's because when I was implementing Login(), I didn't need a JWT because I didn't have one. But if you needed PostWithJwt(), you have the tools to do it now. It doesn't look like this API actually uses any other POST methods. It does use HEAD, GET, and PUT, though, so you'll need to implement those.
This is how I write web API clients. First, I study the documentation to see what I need to do to use a particular endpoint. Then, I ask if my current RestClient class can satisfy those capabilities. If not, I implement a new RestClient method that can. When RestClient has enough features, I look at my "API" type and decide how to put a friendly function around the HTTP. Usually that involves making a new JSON object for inputs and outputs. The API method never does anything complex, it passes the URL, JSON, and any parameters to the RestClient and gets JSON back. Then it deserializes the JSON to an object and returns something.
That way, my application worries about "What things am I doing" rather than "Oh wait to search for series I first need to configure a GET request..."
If this code doesn't work in some way, post as many details about what's going wrong as you can. I can try to remote debug it. I left out any real attempt at handling errors, so it'll faceplant if you do something the API doesn't like. Error handling complicates tutorials.
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
Tags for this Thread
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|