Get the Code
This guided tour provides links to the mentioned source files in our repository, so you don't need to check out the source to follow along. But if you want, you can get the source from http://gitorious.org/projects/sputnik/repos/mainline. This page does not assume that you've installed Sputnik. But if you want to install it from source, you should see Source.)
Components
Sputnik's code is organized into a number of directories, which correspond to components (or, to be more precise, rocks.) Most of those are plugins and we'll be ignoring them. The only three we'll be looking at are "sputnik", "saci" and "versium". Each has a subdirectory called "lua" which contains the actual Lua code.
Sputnik
Start with "sputnik", assuming for now that "saci" does two things for you automagically:
- saving and retrieving nodes,
- keeping track of history,
- "inflating" and "activating" nodes.
The latter means that depending on the type of node, when Sputnik gets it,
node.content may be equal to a string (the default) or a table of data.
Let's ignore for now how this works. Just trust me that when we get a node
like "_config", with node = sputnik:get_node("_config"), we can then do things
like node.content.HOME_PAGE to get variables defined in the body of the node.
sputnik.cgi
Sputnik can be launched in two ways: through CGILua and WSAPI. I'll focus on WSAPI here,
this is the direction in which we are moving. So, let's look at sputnik.cgi. This file is not in the repository per se - it is created
during the installation. It looks something like:
#! /home/yuri/sputnik/bin/lua5.1
require("luarocks.require")
require("wsapi.cgi")
require("sputnik.wsapi_app")
local my_app = sputnik.wsapi_app.new{
VERSIUM_PARAMS = { '/home/yuri/sputnik/wiki-data/' },
BASE_URL = '/cgi-bin/sputnik.cgi',
PASSWORD_SALT = 'pFgjX6juDYbpOOOyLOAzQtJDesfi1Cxg7EqVBzg4',
TOKEN_SALT = 'nhk1VYqxDRWjawNfOSnFkLMig34IsaDGhNy3CPr6',
}
wsapi.cgi.run(my_app)
In other words, we instantiate a new WSAPI application and then run it.
A WSAPI Application
A WSAPI application is a object that can be called by WSAPI to handle HTTP requests. The code for that is in sputnik/wsapi_app.lua, but it is not particularly interesting. What this code really does is put a particular API (WSAPI) on a more generic Sputnik class. It also provides error handling.
The Sputnik Class
The "sputnik" class is where things really happen. Let's look at sputnik/init.lua
(sputnik/lua/sputnik/init.lua, to be more precise).
The module exports a method new() which can be used to create a new instance of Sputnik. The instance's main method is handle_request(), which takes WSAPI request and response as parameters.
(From this point on, the text may need a little updating.)
- It calls
self:translate_request(request)to fill in the missing requests and to do authentication.- Let's look quickly at
translate_request()-- it accepts a WSAPI request, and the pre-processes the request parameters, doing two things main things:- It checks of a presense of parameter called "post_token" and unhashes them. This is a part of how we block link spam.
- It authenticates the user, so that
request.userends up either being set to authenticated user name or to nil. Note that the actual authentication is done by a module that is pluggable.
- Let's look quickly at
- It loads a node and "activates" it
- It uses the suffix attached to the node name (e.g., "Node.edit") to determine what "action" it needs and looks up that action, then calls it.
Now let's read the rest of Sputnik class definition. Ignore make_url(), make_link(), add_urls(), add_links().
Focus on Sputnik:new(),
Sputnik:init() and Sputnik:activate_node(). Note that most of the work is offloaded to versium.smart.repository and
versium.smart.smartnode (soon to be renamed saci). activate_nodes() adds some functionality to node that goes beyond
what the Repository can do for us. This includes loading translations and actions.
Actions
Read parts of sputnik/actions/wiki.lua
(or, rather sputnik/lua/sputnik/actions/wiki.lua). It's a long file, but note
that all functions in it have the same signature: they all accept
three parameters: a node, a request (which includes the parameters in
request.params) and a pointer to sputnik. They return a string and
(optionally) a content type. So, basically, when the user asks for
SomeNode.foo (note that asking for just "SomeNode" is interpreted as
asking for "SomeNode.show"), we load "SomeNode" and then call
wiki.actions.foo() with this node, the request, and an instance of
sputnik as parameters. Don't read all of wiki.lua - just enough to
get the pattern. Then have a look at
sputnik/actions/css.lua to see
that we can have them in more than one file.
The Nodes
Now look at @Root.raw. Note that this is what actually gets saved - it's a lua file that gets evaluated into a table.
This is a node from which all nodes inherit. Scroll to the bottom and look at where the variable "actions" is set:
actions= [=[show = "wiki.show"
show_content = "wiki.show_content"
history = "wiki.history"
edit = "wiki.edit"
post = "wiki.post"
rss = "wiki.rss"
diff = "wiki.diff"
code = "wiki.code"
raw = "wiki.raw"
raw_content = "wiki.raw_content"
login = "wiki.show_login_form"
sputnik_version = "wiki.sputnik_version"
]=]
So, I lied: if you ask for "SomeNode.history" we don't automatically call "wiki.actions.history". We actually look in the "actions" field of the node. We can redefine it either for all nodes (by editing @Root) or for some nodes, by editing their "action" field individually or by having them all inherit from the same node. E.g.: @Ticket.raw has:
actions= [=[show = "wiki.edit"]=]
This means that for a node that inherits from @Ticket, asking for
Node.show will actually call wiki.actions.edit! Sneaky!
Back to @Root. There is another interesting field in it: "fields":
fields= [=[-- Think twice before editing this ------------------------
fields = {0.0, proto="concat", activate="lua"}
title = {0.1 }
category = {0.2 }
actions = {0.3, proto="concat", activate="lua"}
config = {0.4, proto="concat" }
templates = {0.5, proto="concat",
activate="node_list"}
translations = {0.51, proto="concat",
activate="node_list"}
prototype = {0.6 }
permissions = {0.7, proto="concat"}
content = {0.8 }
edit_ui = {0.9, proto="concat"}
...
]=]
This is is a special field that is used by the versium.smart (now "SACI")
to "activate" the node.
For instance, we have in it:
fields = [=[
...
actions = {0.3, proto="concat", activate="lua"}
...
]=]
this means: inherit the value of "actions" from the prototype, by concattenating the local value of "actions" (the one actually saved in the node) with the prototype values.
E.g.: RawDotFile, a part of Graphviz Demo, says:
actions= [=[show="wiki.code"
svg="graphviz.dot2svg"
png="graphviz.dot2png"
gif="graphviz.dot2gif"]=]
It doesn't specify an explicit prototype, so it inherits from "@Root"
@Root says:
actions= [=[show = "wiki.show"
show_content = "wiki.show_content"
history = "wiki.history"
edit = "wiki.edit"
post = "wiki.post"
rss = "wiki.rss"
diff = "wiki.diff"
code = "wiki.code"
raw = "wiki.raw"
raw_content = "wiki.raw_content"
login = "wiki.show_login_form"
sputnik_version = "wiki.sputnik_version"
]=]
Since fields.actions.proto is set to "concat", we put the two together:
show = "wiki.show"
show_content = "wiki.show_content"
history = "wiki.history"
edit = "wiki.edit"
post = "wiki.post"
rss = "wiki.rss"
diff = "wiki.diff"
code = "wiki.code"
raw = "wiki.raw"
raw_content = "wiki.raw_content"
login = "wiki.show_login_form"
sputnik_version = "wiki.sputnik_version"
show="wiki.code"
svg="graphviz.dot2svg"
png="graphviz.dot2png"
gif="graphviz.dot2gif"
Then we check fields.actions.activate. It says "lua". This means:
use the function provided by versium.inflators.lua. We run the string
through that function and it gives us back a table. Note that in that
table "show" field is set to "wiki.source", since local actions come
after the prototypes actions.
We can also look at the edit_ui field in @Root. This specifies what the
edit form would look like. We can change this - see @Ticket.
Versium
Now we get to versium and versium.smart (=SACI). Versium itself is
very simple - just read versium/lua/versium.lua.
It's just a simple façade to several modules that do the actual storage.
versium.smart is tricky.
Most of what it does is in
versium.smart.smartnode.
Don't look at the code until you get a general idea of how it is used.
But if you understood everything so far, then you are ready. Basically, it's
a module that sits on top of versium, assumes that versium is storing
Lua code that evaluates into a table of strings, then looks for a few
special fields, such as "proto" and "fields". It uses the information
in those fields to do inheritance and to figure out what to do with
the rest of the fields. It keeps the original values (straight out of
versium) by pushing them into metatables. It knows how to accept a
list of parameters, and generate an updated payload to be stored into
versium (SmartNode:update())