The ‘webapp’ model of Kaya
Kaya has built-in support for web application development. In this tutorial, we will create a couple of trivial web applications which demonstrate Kaya’s webapp development model, and three of its main strengths:
- Abstract representation of HTML to improve output code quality and easily separate execution order from output code order.
- Simple and secure passing of state through the webapp.
- Automatic filling of forms to make form validation easier.
Hello web!
To begin, let us look at the simplest program, “Hello world!”, adapted to run as a CGI web application. Put the following in a file called webhello.k
webapp webhello;
import Webapp;
import HTMLDocument;
HTMLDocument webmain() {
doc = HTMLDocument::new(HTML4Strict,"Hello World!");
h1 = addHeading(doc.body,1,"Hello World!");
return doc;
}
new creates a blank HTML document with the specified document type (HTML4Strict
is recommended, but you can use XHTML1Strict
if you really must) and document title (which may not be empty). The Transitional doctypes are not supported – since no Kaya-generated page will be from before the release of HTML 4, there is no need to support this old transition.
The addHeading function adds a level 1 (i.e. main) heading element with initial text “Hello World!” to the current end of the body of the document.
In a real document, you would keep building things up, adding paragraphs, lists, tables and so on. You can also add elements within elements, add extra text to elements, and so on, like this.
// ...
p = addParagraph(doc.body,"This is an ");
emphasise = appendInlineElement(p,StrongEmphasis,"important");
addString(p," paragraph!");
// outputs <p>This is an <em>important</em> paragraph!</p>
// ...
Compiling the webapp file with kayac webhello.k creates an executable, hello.cgi
. Kaya knows that it should produce a CGI program because of the header webapp webhello
, and expects to find a webmain
function, which is run to generate the HTMLDocument. The webapp then takes the output of this function to send a document to the web browser.
You can of course run this from the command line, but it is more likely that you will want to put it somewhere where your web server knows to execute it as a CGI script.
Webapp’s HTML document model
Webapp uses the HTMLDocument module to describe the web page. This treats the web page as a tree structure (since HTML describes a tree structure) and provides a range of functions to build and query that structure. When the tree is returned, Kaya iterates over the tree to print it. Because of the nature of CGI, you should not use the putStr, or putStrLn functions or any function that uses them within a webapp.
Producing the document in this way has the advantage that you will never accidentally forget to include a closing tag or quote an attribute, and many common mistakes that would make your HTML document invalid are also prevented (not all, yet…)
You can use the readFromString function to convert String
s into the tree structure used by the HTMLDocument module. This function has conversion settings that you can use to restrict the legal elements and attributes, which can be useful for safely converting user-supplied data.
Forms and Event Handlers
The webapp system includes several abstractions to make developing web programs easy in addition to the above. These are mainly concerned with passing state and other information between invocations of the webapp. HTTP is a stateless protocol, but for web programs, it is usually essential to pass state between two invocations.
To do this, use the
addLocalControlInput function (for forms) or the localControlURL function (for links). As well as the usual information of where the element goes, they take information to pass state through.
ElementTree addLocalControlInput(ElementTree parent, String label, b(a) fn,a state)
The two last arguments are fn
, the function which will be called when this form’s submit button is pressed (the event handler), and state
, which is the state which will be passed to the function. The state argument is a useful place to put any hidden variables which need to be passed through; it can be any type, provided that it can be marshalled. This argument may not be needed, in which case you can simply pass through a dummy value.
We can create a simple form with the following function:
ElementTree helloForm() {
parent = anonymousBlock;
form = addLocalForm(parent);
f1 = addFieldset(form,"Who are you?");
input = addLabelledInput(f1,"Your First Name",InputText,"fname","",0);
input = addLabelledInput(f1,"Your Last Name",InputText,"lname","",0);
submit = addLocalControlInput(f1,"Say hello",OnHello,1);
return form;
}
This submits a form, with a request that the form data be processed by the OnHello
function we define below. Note that since we don’t need to pass any state data, we just use a dummy value here.
ElementTree OnHello(Int dummy) {
div = anonymousBlock;
name = incomingValue("fname",DataPost)+" "+incomingValue("lname",DataPost);
addParagraph(div,"Hello "+name);
return div;
}
OnHello
gets the value of name from the HTTP POST variables, and says hello. incomingValueis one of the common functions to the webapp and CGI models.
Finally, the main webapp needs to know to display the form, and call OnHello
– unlike the CGI model this is not automatic – you must select a point in the function to call it using the runHandler function. Note that the return type of the function(s) called by any particular runHandler
must be identical, and identical to the return type of the default function.
webapp helloform;
import Webapp;
import HTMLDocument;
HTMLDocument webmain() {
doc = newDocument(HTML4Strict,"Hello program");
h1 = addHeading(doc.body,"Hello Program");
result = runHandler(@HelloForm);
appendExisting(doc.body,result);
return doc;
}
Additional form handling
Suppose in the above example we wanted to check that both fields were filled in before saying hello. Modifying OnHello
as follows will do this, and redisplay the form if it’s not complete.
ElementTree OnHello(Int dummy) {
div = anonymousBlock; // we create this temporarily.
fname = incomingValue("fname",DataPost);
lname = incomingValue("lname",DataPost);
if (fname != "" && lname != "") {
p = addParagraph(div,"Hello "+fname+" "+lname);
return p;
} else {
form = HelloForm();
autoFill(form,DataPost);
return form;
}
}
The autoFill()
function (from the Webapp module) fills the form with values from POST data, so if one of the fields was completed, this value is placed back into the form. You can use this to trivially write a fully-validated form in a user-friendly way.
Having multiple calls to runHandler in the same webapp
While any particular runHandler
call can only return data of one type, you can in theory have multiple runHandler
calls in the same webapp. If you do, then you must select which runHandler
to use at runtime without reference to user-supplied data.
Never do this:
if (incomingExists("foo",DataGet)) {
TypeA foo = runHandler(@defaultFoo);
} else {
TypeB bar = runHandler(@defaultBar);
}
In this situation, a user can manipulate whether the incoming variable foo
exists, and so cause the wrong handler function to be called. The handler function will then have the wrong return type at run-time, which can cause your program to crash (and may introduce potentially serious security problems). Compiling with -nortchecks
makes such a crash more likely but they can happen anyway.
The better way to do this is to use a single runHandler
function, but to make its return type more informative. For example:
// ...
data FooBar = IsFoo(Foo foo) | IsBar(Bar bar);
// ...
foobar = runHandler(@defaultfn);
case foobar of {
IsFoo(foo) -> // do something with foo
| IsBar(bar) -> // do something with bar
}
This ensures that the type information will be correct at run-time.
Different return types for webmain
The webmain
function does not have to return HTMLDocument – it can return any type for which a displayPage function exists. These functions exist already for the following types:
HTMLDocument
: alwaysString
: alwaysBinary
: alwaysImages
: (if the optional Image library is compiled)
Because any return type needs to be converted to a String
or Binary
block before sending to the web browser, it is easy to write displayPage
functions for any type.
data WebReturn = HTML(HTMLDocument h) | PNGimage(WebImage p);
WebReturn webmain() {
// ...
}
Void displayPage(WebReturn web) {
case web of {
HTML(h) -> displayPage(h);
| PNGimage(p) -> displayPage(p);
}
}
A webapp using the above and source code are available as a minimal example of this method.