Practical Racket: Using a JSON REST API
This first post explains how to do simple HTTP GETs to a JSON REST API, and then parse the JSON response into a form that you can traverse and manipulate in Racket.
Simple HTTP GETs
I started this off by looking for a super simple function that takes a URL and returns the body of the
response as a string. Something like Python's
urllib.urlopen. The closest thing I found was the
call/input-url function from the
It works like so
(require net/url) (call/input-url (string->url "https://www.ifixit.com/api/2.0/guides/13470") get-pure-port port->string)
That snippet will return a string of the JSON representation of The Orange Teardown on iFixit.com.
It's cool that it works, but
port->string could use some more explanation.
string->url takes a string, and returns a url struct that contains fields for each segment of the url
string->url a full URL like the one above will return a populated url struct, while
(string->url "index.html") will return a url struct with only the path field populated with "index.html".
get-pure-port is a little misleading if you're not familiar with Racket. My initial thought was that it meant a TCP/UDP port, but in Racket, a port is something that produces or consumes bytes. This is essentially a stream or buffer used with which to fill the response. As far as the difference between an pure port and an impure port, here's a quote from the Racket docs.
A pure port is one from which the MIME headers have been removed, so that what remains is purely the first content fragment. An impure port is one that still has its MIME headers.
By passing the procedure
call/input-url, we're specifying that we do not need the MIME headers.
port->string is a procedure that converts a port to a string. Realistically you could pass a procedure that takes a port and returns anything, and that would be the output of
Here's an example of passing a procedure that takes a port and returns a jsexpr, Racket's representation of a JSON document.
(require json) (require net/url) (call/input-url (string->url "https://www.ifixit.com/api/2.0/guides/13470") get-pure-port (lambda (port) (string->jsexpr (port->string port))))
Now the output of this function is a jsexpr, instead of a string.
Just to make this a little cleaner, we can make use of
compose, which takes a variadic list of procedures, and composes them together.
(compose string->jsexpr port->string) returns a procedure that looks like
(lambda (x) (string->jsexpr (port->string x))))
so you're basically creating a new abstraction that calls the last procedure in the list of arguments, then passes that result to the second to last procedure in the list of arguments, until you get to the first procedure in the list, which returns it's result. The idea isn't too different from piping things together in a shell command. If we were planning on using this procedure a lot we could even give it a name like
(define port->jsexpr (compose string->jsexpr port->string)), but we're using it once, so whatever.
To add a little more abstraction, we make a simpler function that just takes a string url and returns a jsexpr.
(require net/url) (require json) (define (get-json url) (call/input-url (string->url url) get-pure-port (compose string->jsexpr port->string))) ;; Returns a parsed JSON expression (get-json "https://www.ifixit.com/api/2.0/guides/13470")
Hooray! Now we have the tools to download and parse a JSON document from whichever service you wish. We just need to know how to select things from the JSON document.
Now that we have the JSON in a form we can deal with, we need to know how to operate on it. The Racket JSON docs give a pretty good explanation of which Racket types are used to represent corresponding types in JSON. Lists are lists, booleans are booleans, strings are strings, numbers are numbers, and objects are hashes. There are a lot of things you can do with hashes in Racket, so I'd recommend checking out that doc page. For a quick example, you can get the value at a key by using
(hash-ref hash 'keyname).
If we wanted to get the username of the author of the guide, we could do
(define guide-data (get-json "https://www.ifixit.com/api/2.0/guides/13470")) (hash-ref (hash-ref guide-data 'author) 'username)
This grabs the value of the key author and then grabs the value of username from that.
For a little more complex example we can grab the names of all tools used in the iPhone 7 Teardown.
#lang racket (define IPHONE_7_TEARDOWN "https://www.ifixit.com/api/2.0/guides/67382") (require net/url) (require json) (define (get-json url) (call/input-url (string->url url) get-pure-port (compose string->jsexpr port->string))) (define guide-data (get-json API_URL)) (map (lambda (tool) (hash-ref tool 'text)) (hash-ref guide-data 'tools))
The result of this should be
'("64 Bit Driver Kit" "Spudger" "Tweezers" "iFixit Opening Picks set of 6" "iSclack")
With the procedures described in this post you should be able query API endpoints in general and extract information from them. In order to do a POST request, or add Headers, it'll take a different procedure, which I'll cover in another post as soon as I figure out how to use it.
I realized that you can actually use a procedure called
read-json instead of
(compose string->jsexpr post->string) as the handler argument for
read-json takes a port as input and returns a jsexpr as output, which handles the functionality that our
(compose string->jsexpr port->string) procedure was made for.