Login or register Large RSS Icon

Source Guided Tour

This page guides you through Sputnik source code should you want to read it.

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. Note that we'll be ignoring everything in it except for two subdirectories: "sputnik" and "versium". (Note that this page does not assume that you've installed Sputnik. But if you want to install it from source, you should see Source.)

Start with "sputnik", assuming for now that "versium" does two things for you automagically:

  1. saving and retrieving nodes,
  2. keeping track of history,
  3. "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 by install.sh. It looks something like:

#! /home/yuri/sputnik/bin/lua5.1                             
require'luarocks.require'                               
require'wsapi.cgi'
require'sputnik'                    
SPUTNIK_CONFIG = {                                      
   VERSIUM_PARAMS = { dir = "/home/yuri/sputnik/wiki-data/" },
   BASE_URL       = '/cgi-bin/sputnik.cgi'              
}                                                       
wsapi.cgi.run(sputnik.wsapi_run)  

In other words, we require sputnik and wsapi then call wsapi.cgi.run() giving it one of sputnik's functions as a parameter. WSAPI will call this function every time it needs to process a request. Note that we also set a global SPUTNIK_CONFIG - that's our bootstrapping configuration.

sputnik/init.lua

Now let's look at sputnik/init.lua (sputnik/lua/sputnik/init.lua, to be more precise). Follow the flow of execution starting with wsapi_run() - the function that wsapi will call.

Follow the order of execution, which goes as follows:

  1. WSAPI calls sputnik.wsapi_run(wsapi_env)
  2. wsapi_run() calls sputnik.protected_run() (a function that runs sputnik for one request catching all errors)
  3. protected_run() calls sputnik.unprotected_run() (same but throws errors)
  4. unprotected_run() creates an instance of Sputnik(), and then calls Sputnik:run() (note that this will change slightly - we'll start keeping track of Sputnik instances between requests).

Sputnik:run() is the function you should read most carefully:

  1. 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:
      1. It checks of a presense of parameter called "post_token" and unhashes them. This is a part of how we block link spam.
      2. It authenticates the user, so that request.user ends up either being set to authenticated user name or to nil. Note that the actual authentication is done by a module that is pluggable.
  2. It loads a node and "activates" it
  3. 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())

Powered by Sputnik | XHTML 1.1