doc: Add tutorial
authorPetri Lehtinen <petri@digip.org>
Sat, 17 Oct 2009 11:18:40 +0000 (14:18 +0300)
committerPetri Lehtinen <petri@digip.org>
Sat, 17 Oct 2009 11:18:40 +0000 (14:18 +0300)
doc/apiref.rst
doc/github_commits.c [new file with mode: 0644]
doc/index.rst
doc/tutorial.rst [new file with mode: 0644]

index 9c28fb8..3ba5fbf 100644 (file)
@@ -1,3 +1,5 @@
+.. _apiref:
+
 *************
 API Reference
 *************
@@ -113,6 +115,8 @@ functions:
    and false for values of other types and for *NULL*.
 
 
+.. _apiref-reference-count:
+
 Reference Count
 ---------------
 
diff --git a/doc/github_commits.c b/doc/github_commits.c
new file mode 100644 (file)
index 0000000..5af01f0
--- /dev/null
@@ -0,0 +1,164 @@
+#include <stdlib.h>
+#include <string.h>
+
+#include <jansson.h>
+#include <curl/curl.h>
+
+#define BUFFER_SIZE  (256 * 1024)  /* 256 KB */
+
+#define URL_FORMAT   "http://github.com/api/v2/json/commits/list/%s/%s/master"
+#define URL_SIZE     256
+
+/* Return the offset of the first newline in text or the length of
+   text if there's no newline */
+static int newline_offset(const char *text)
+{
+    const char *newline = strchr(text, '\n');
+    if(!newline)
+        return strlen(text);
+    else
+        return (int)(newline - text);
+}
+
+struct write_result
+{
+    char *data;
+    int pos;
+};
+
+static size_t write_response(void *ptr, size_t size, size_t nmemb, void *stream)
+{
+    struct write_result *result = (struct write_result *)stream;
+
+    if(result->pos + size * nmemb >= BUFFER_SIZE - 1)
+    {
+        fprintf(stderr, "error: too small buffer\n");
+        return 0;
+    }
+
+    memcpy(result->data + result->pos, ptr, size * nmemb);
+    result->pos += size * nmemb;
+
+    return size * nmemb;
+}
+
+static char *request(const char *url)
+{
+    CURL *curl;
+    CURLcode status;
+    char *data;
+    long code;
+
+    curl = curl_easy_init();
+    data = malloc(BUFFER_SIZE);
+    if(!curl || !data)
+        return NULL;
+
+    struct write_result write_result = {
+        .data = data,
+        .pos = 0
+    };
+
+    curl_easy_setopt(curl, CURLOPT_URL, url);
+    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_response);
+    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &write_result);
+
+    status = curl_easy_perform(curl);
+    if(status != 0)
+    {
+        fprintf(stderr, "error: unable to request data from %s:\n", url);
+        fprintf(stderr, "%s\n", curl_easy_strerror(status));
+        return NULL;
+    }
+
+    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
+    if(code != 200)
+    {
+        fprintf(stderr, "error: server responded with code %ld\n", code);
+        return NULL;
+    }
+
+    curl_easy_cleanup(curl);
+    curl_global_cleanup();
+
+    /* zero-terminate the result */
+    data[write_result.pos] = '\0';
+
+    return data;
+}
+
+int main(int argc, char *argv[])
+{
+    unsigned int i;
+    char *text;
+    char url[URL_SIZE];
+
+    json_t *root;
+    json_error_t error;
+    json_t *commits;
+
+    if(argc != 3)
+    {
+        fprintf(stderr, "usage: %s USER REPOSITORY\n\n", argv[0]);
+        fprintf(stderr, "List commits at USER's REPOSITORY.\n\n");
+        return 2;
+    }
+
+    snprintf(url, URL_SIZE, URL_FORMAT, argv[1], argv[2]);
+
+    text = request(url);
+    if(!text)
+        return 1;
+
+    root = json_loads(text, &error);
+    free(text);
+
+    if(!root)
+    {
+        fprintf(stderr, "error: on line %d: %s\n", error.line, error.text);
+        return 1;
+    }
+
+    commits = json_object_get(root, "commits");
+    if(!commits || !json_is_array(commits))
+    {
+        fprintf(stderr, "error: commits is not an array\n");
+        return 1;
+    }
+
+    for(i = 0; i < json_array_size(commits); i++)
+    {
+        json_t *commit, *id, *message;
+        const char *message_text;
+
+        commit = json_array_get(commits, i);
+        if(!json_is_object(commit))
+        {
+            fprintf(stderr, "error: commit %d is not an object\n", i + 1);
+            return 1;
+        }
+
+        id = json_object_get(commit, "id");
+        if(!id || !json_is_string(id))
+        {
+            fprintf(stderr, "error: commit %d: id is not a string\n", i + 1);
+            return 1;
+        }
+
+        message = json_object_get(commit, "message");
+        if(!message || !json_is_string(message))
+        {
+            fprintf(stderr, "error: commit %d: message is not a string\n", i + 1);
+            return 1;
+        }
+
+        message_text = json_string_value(message);
+        printf("%.8s %.*s\n",
+               json_string_value(id),
+               newline_offset(message_text),
+               message_text);
+    }
+
+    json_decref(root);
+    return 0;
+}
index 97ff155..d4a29a8 100644 (file)
@@ -11,6 +11,7 @@ This is the documentation for Jansson_ |release|, last updated |today|.
    :maxdepth: 2
 
    gettingstarted
+   tutorial
    apiref
 
 
diff --git a/doc/tutorial.rst b/doc/tutorial.rst
new file mode 100644 (file)
index 0000000..df930a7
--- /dev/null
@@ -0,0 +1,270 @@
+.. _tutorial:
+
+********
+Tutorial
+********
+
+.. highlight:: c
+
+In this tutorial, we create a program that fetches the latest commits
+of a repository in GitHub_ over the web. One of the response formats
+supported by `GitHub API`_ is JSON, so the result can be parsed using
+Jansson.
+
+To stick to the the scope of this tutorial, we will only cover the the
+parts of the program related to handling JSON data. For the best user
+experience, the full source code is available:
+:download:`github_commits.c`. To compile it (on Unix-like systems with
+gcc), use the following command::
+
+    gcc -o github_commits github_commits.c -ljansson -lcurl
+
+libcurl_ is used to communicate over the web, so it is required to
+compile the program.
+
+The command line syntax is::
+
+    github_commits USER REPOSITORY
+
+``USER`` is a GitHub user ID and ``REPOSITORY`` is the repository
+name. Please note that the GitHub API is rate limited, so if you run
+the program too many times within a short period of time, the sever
+starts to respond with an error.
+
+.. _GitHub: http://github.com/
+.. _GitHub API: http://develop.github.com/
+.. _libcurl: http://curl.haxx.se/
+
+
+.. _tutorial-github-commits-api:
+
+The GitHub Commits API
+======================
+
+The GitHub commits API is used by sending HTTP requests to URLs
+starting with ``http://github.com/api/v2/json/commits/``. Our program
+only lists the latest commits, so the rest of the URL is
+``list/USER/REPOSITORY/BRANCH``, where ``USER``, ``REPOSITORY`` and
+``BRANCH`` are the GitHub user ID, the name of the repository, and the
+name of the branch whose commits are to be listed, respectively. The
+following definitions are used to build the request URL::
+
+   #define URL_FORMAT   "http://github.com/api/v2/json/commits/list/%s/%s/master"
+   #define URL_SIZE     256
+
+GitHub responds with a JSON object of the following form:
+
+.. code-block:: none
+
+    {
+        "commits": [
+            {
+                "id": "<the commit ID>",
+                "message": "<the commit message>",
+                <more fields, not important to this tutorial>
+            },
+            {
+                "id": "<the commit ID>",
+                "message": "<the commit message>",
+                <more fields, not important to this tutorial>
+            },
+            <more commits...>
+        ]
+    }
+
+In our program, the HTTP request is sent using the following
+function::
+
+    static char *request(const char *url);
+
+It takes the URL as a parameter, preforms a HTTP GET request, and
+returns a newly allocated string that contains the response body. For
+full details, refer to :download:`the code <github_commits.c>`, as the
+actual implementation is not important here.
+
+
+.. _tutorial-the-program:
+
+The Program
+===========
+
+First the includes::
+
+    #include <string.h>
+    #include <jansson.h>
+
+Like all the programs using Jansson, we need to include
+:file:`jansson.h`.
+
+The following function is used when formatting the result to find the
+first newline in the commit message::
+
+    /* Return the offset of the first newline in text or the length of
+       text if there's no newline */
+    static int newline_offset(const char *text)
+    {
+        const char *newline = strchr(text, '\n');
+        if(!newline)
+            return strlen(text);
+        else
+            return (int)(newline - text);
+    }
+
+The main function follows. In the beginning, we first declare a bunch
+of variables and check the command line parameters::
+
+    unsigned int i;
+    char *text;
+    char url[URL_SIZE];
+
+    json_t *root;
+    json_error_t error;
+    json_t *commits;
+
+    if(argc != 3)
+    {
+        fprintf(stderr, "usage: %s USER REPOSITORY\n\n", argv[0]);
+        fprintf(stderr, "List commits at USER's REPOSITORY.\n\n");
+        return 2;
+    }
+
+Then we build the request URL using the user and repository names
+given as command line parameters::
+
+    snprintf(url, URL_SIZE, URL_FORMAT, argv[1], argv[2]);
+
+This uses the ``URL_SIZE`` and ``URL_FORMAT`` constants defined above.
+Now we're ready to actually request the JSON data over the web::
+
+    text = request(url);
+    if(!text)
+        return 1;
+
+If an error occurs, our function ``request`` prints the error and
+returns *NULL*, so it's enough to just return 1 from the main
+function.
+
+Next we'll call :cfunc:`json_loads()` to decode the JSON text we got
+as a response::
+
+    root = json_loads(text, &error);
+    free(text);
+
+    if(!root)
+    {
+        fprintf(stderr, "error: on line %d: %s\n", error.line, error.text);
+        return 1;
+    }
+
+We don't need the JSON text anymore, so we can free the ``text``
+variable right after decoding it. If :cfunc:`json_loads()` fails, it
+returns *NULL* and sets error information to the :ctype:`json_error_t`
+structure given as the second parameter. In this case, our program
+prints the error information out and returns 1 from the main function.
+This check is really only to be sure, because we can assume that the
+GitHub API returns correct JSON to us.
+
+Next, we'll extract the ``commits`` array from the JSON response::
+
+    commits = json_object_get(root, "commits");
+    if(!commits || !json_is_array(commits))
+    {
+        fprintf(stderr, "error: commits is not an array\n");
+        return 1;
+    }
+
+This is the array that contains objects describing latest commits in
+the repository. If the key ``commits`` doesn't exist,
+:cfunc:`json_object_get()` returns *NULL*. We also check that the
+returned value really is an array.
+
+Then we proceed to loop over all the commits in the array::
+
+    for(i = 0; i < json_array_size(commits); i++)
+    {
+        json_t *commit, *id, *message;
+        const char *message_text;
+
+        commit = json_array_get(commits, i);
+        if(!json_is_object(commit))
+        {
+            fprintf(stderr, "error: commit %d is not an object\n", i + 1);
+            return 1;
+        }
+    ...
+
+The function :cfunc:`json_array_size()` returns the size of a JSON
+array. First, we again declare some variables and then extract the
+i'th element of the ``commits`` array using :cfunc:`json_array_get()`.
+We also check that the resulting value is a JSON object. (The
+structure of the response JSON was explained in
+:ref:`tutorial-github-commits-api`).
+
+Next we'll extract the commit ID and commit message, and check that
+they both are JSON strings::
+
+        id = json_object_get(commit, "id");
+        if(!id || !json_is_string(id))
+        {
+            fprintf(stderr, "error: commit %d: id is not a string\n", i + 1);
+            return 1;
+        }
+
+        message = json_object_get(commit, "message");
+        if(!message || !json_is_string(message))
+        {
+            fprintf(stderr, "error: commit %d: message is not a string\n", i + 1);
+            return 1;
+        }
+    ...
+
+And finally, we'll print the first 8 characters of the commit ID and
+the first line of the commit message. A C-style string is extracted
+from a JSON string using :cfunc:`json_string_value()`::
+
+        message_text = json_string_value(message);
+        printf("%.8s %.*s\n",
+               json_string_value(id),
+               newline_offset(message_text),
+               message_text);
+    }
+
+After sending the HTTP request, we decoded the JSON text using
+:cfunc:`json_loads()`, remember? It returns a *new reference* to a
+JSON value it decodes. When we're finished with the value, we'll need
+to decrease the reference count using :cfunc:`json_decref()`. This way
+Jansson can release the resources::
+
+    json_decref(root);
+    return 0;
+
+For a detailed explanation of reference counting in Jansson, see
+:ref:`apiref-reference-count` in :ref:`apiref`.
+
+The program's ready, let's test it and view the latest commits in
+Jansson's repository::
+
+    $ ./github_commits akheron jansson
+    86dc1d62 Fix indentation
+    b67e130f json_dumpf: Document the output shortage on error
+    4cd77771 Enhance handling of circular references
+    79009e62 json_dumps: Close the strbuffer if dumping fails
+    76999799 doc: Fix a small typo in apiref
+    22af193a doc/Makefile.am: Remove *.pyc in clean
+    951d091f Make integer, real and string mutable
+    185e107d Don't use non-portable asprintf()
+    ca7703fb Merge branch '1.0'
+    12cd4e8c jansson 1.0.4
+    <etc...>
+
+
+Conclusion
+==========
+
+In this tutorial, we implemented a program that fetches the latest
+commits of a GitHub repository using the GitHub commits API. Jansson
+was used to decode the JSON response and to extract the commit data.
+
+This tutorial only covered a small part of Jansson. For example, we
+did not create or manipulate JSON values at all. Proceed to
+:ref:`apiref` to explore all features of Jansson.