One thing that surprises many people when they come to Factor, is that a lot of the Factor infrastructure (main site, planet, pastebin, documentation, and wiki) is written in Factor, and runs on a Factor web server.
The Factor web server is very capable, supporting static files, CGI scripts, SSL authentication, session management, and dynamic web pages. Some of the vocabularies that are involved:
- http.server, the web server
- furnace, the web framework
- db, the database support
- html.templates (both chloe and fhtml)
- html.forms and form validators
Hello, world!
This is a simple application that returns a plain text page that says "Hello, world!". Our web application is structured into a dispatcher (our "main responder"), an action, and words to create and run the web server.
USING: accessors furnace.actions http.server http.server.dispatchers http.server.responses io.servers kernel namespaces ; IN: webapps.hello TUPLE: hello < dispatcher ; : <hello-action> ( -- action ) <page-action> [ "Hello, world!" "text/plain" <content> ] >>display ; : <hello> ( -- dispatcher ) hello new-dispatcher <hello-action> "" add-responder ; : run-hello ( -- ) <hello> main-responder set-global 8080 httpd wait-for-server ; MAIN: run-hello
Run the code by calling run-hello
, then navigate to http://localhost:8080 and you will see the response.
Templates
To begin experimenting with templates, lets change the logic to include a form where a name can be provided. We will create a Chloe template file. Let's create a hello.xml
file in the same location as the webapps.hello
vocabulary:
<?xml version='1.0' ?> <t:chloe xmlns:t="http://factorcode.org/chloe/1.0"> <t:form t:action="$hello"> <label>What is your name?</label> <t:field t:name="name" /> <input type="submit" /> </t:form> </t:chloe>
Now, modify the hello-action
to load the template. The default form submission is via POST
and can be supported using the submit
slot of the action
. We respond to a form submission by returning a plain text response saying "Hello, $name!":
USE: formatting : <hello-action> ( -- action ) <page-action> { hello "hello" } >>template [ "name" param "Hello, %s!" sprintf "text/plain" <content> ] >>submit ;
When you navigate to http://localhost:8080, you will see a simple form prompting you to type in a name. After submitting the form, you will see a customized response depending on the name provided.
Form Validation
It is frequently useful to validate parameters that are submitted via forms (e.g., for numbers, e-mail addresses, ranges, required or optional, etc.). To support this, we need to add validation logic for every parameter desired (using words from the validators vocabulary). In this case, the name should be a required parameter:
USE: validators : <hello-action> ( -- action ) <page-action> [ { { "name" [ v-required ] } } validate-params ] >>validate { hello "hello" } >>template [ "name" value "Hello, %s!" sprintf "text/plain" <content> ] >>submit ;
Next, wrap the dispatcher in an <alloy>, which provides support for session-persistence, form validation, and database persistence.
USE: furnace.alloy USE: db.sqlite : <hello> ( -- dispatcher ) hello new-dispatcher <hello-action> "" add-responder "resource:hello.db" <sqlite-db> <alloy> ;
If you navigate to the website now, and don't provide a name
, you'll be redirected back to the form with the validation error specified.
Other tips
There is a development? symbol that can be set to t
to make sure the web server is running the latest code from your application and that errors generate nice stack traces.
Malu has a nice tutorial on Github about building a blog application in Factor.
All of the Factor websites (as well as some nice examples like a "counter", "todo list", "tiny url", and "ip address") are in resource:extra/webapps
.
Hi,
ReplyDeleteThank you, for a concise presentation of web development with factor. I didn't get the database persistence part working. Am I missing something obvious here?
@Peter: When you say "database persistence", I assume you mean the form validation? That just requires the use of "v-required" and "validate-params" and the addition of the "<alloy>" call. It seems to work for me?
ReplyDeleteThe form validation uses the database to persist validation errors across requests. By adding the "<alloy>", you can now also add other kinds of persistence -- maybe tracking the first time each name said hello, and adding a "We first met N seconds ago" message. That might be a simple way to learn database persistence. The Factor documentation has a db.tuples tutorial that seems like a good place to start.
@mrjbq7: when I run the example as written in the blog, I get an "The image refers to a library or symbol that was not found at load time" error. If I remove the line with reference to the database ""resource:hello.db" " it works fine. I got the same error in db.tuples tutorial.
ReplyDelete@Peter: You are probably running on Windows? Factor doesn't come with the DLLs for SQLite, but you can install it if you want -- either from the SQLite project, or download the 32-bit or 64-bit DLLs (you want "sqlite3.dll"). If you copy that to your Factor directory, it should work.
ReplyDeleteThis comment has been removed by the author.
ReplyDelete@mrjbq7: Yes I'm running factor on Windows and that solved the problem. Thank you!
ReplyDeleteThis is probably a Factor-newbie mistake, but perhaps you can help. When running your first example in the listener, everything works fine, but when I try to save it as a file in work/webapps/hello/hello.factor and run it from the command line with `./factor -run=webapps.hello`, I get the error below. What I'm trying do is run a web server in Factor from the command line in the foreground, so is there is another way, I'd be happy to hear suggestions. Thanks!
ReplyDeleteQuotation's stack effect does not match call site
quot [ \ run-hello execute ]
call-site ( -- )
(U) Quotation: [ c-to-factor -> ]
Word: c-to-factor
(U) Quotation: [ [ catchstack* push ] dip call -> catchstack* pop* ]
(O) Word: command-line-startup
(O) Word: wrong-values
(O) Method: M\ object throw
(U) Quotation: [
OBJ-CURRENT-THREAD special-object error-thread set-global
current-continuation -> error-continuation set-global
[ original-error set-global ] [ rethrow ] bi
]
@Ryan: Sorry about that! Every "main method" must have a stack effect of "( -- )", meaning takes no inputs and gives no outputs. In my case, the "run-hello", produces a "http-server" object which caused the traceback you see. I updated the example to use "wait-for-server" which will block until the server finishes serving connections. Can you try again?
ReplyDeleteThanks!
@mrjbq7, thanks that works much better!
ReplyDeleteNow that I got it working, I just put together a Heroku buildpack for Factor that uses your example (with a link back for credit, of course). Check it out if you want to run Factor apps on Heroku: https://github.com/ryanbrainard/heroku-buildpack-factor
Thanks again for the help!