From a00988f6636a4e53b736be8758b63db8d6ca0446 Mon Sep 17 00:00:00 2001 From: Petri Lehtinen Date: Sat, 17 Oct 2009 14:18:40 +0300 Subject: [PATCH] doc: Add tutorial --- doc/apiref.rst | 4 + doc/github_commits.c | 164 +++++++++++++++++++++++++++++++ doc/index.rst | 1 + doc/tutorial.rst | 270 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 439 insertions(+) create mode 100644 doc/github_commits.c create mode 100644 doc/tutorial.rst diff --git a/doc/apiref.rst b/doc/apiref.rst index 9c28fb8..3ba5fbf 100644 --- a/doc/apiref.rst +++ b/doc/apiref.rst @@ -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 index 0000000..5af01f0 --- /dev/null +++ b/doc/github_commits.c @@ -0,0 +1,164 @@ +#include +#include + +#include +#include + +#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; +} diff --git a/doc/index.rst b/doc/index.rst index 97ff155..d4a29a8 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -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 index 0000000..df930a7 --- /dev/null +++ b/doc/tutorial.rst @@ -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": "", + "message": "", + + }, + { + "id": "", + "message": "", + + }, + + ] + } + +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 `, as the +actual implementation is not important here. + + +.. _tutorial-the-program: + +The Program +=========== + +First the includes:: + + #include + #include + +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 + + + +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. -- 2.1.4