LeftParen 0.6 Documentation
Website: http://leftparen.com
LeftParen is a framework for quickly creating web apps. It runs on PLT Scheme v4.1.5. LeftParen is released under an MIT License. The source is available on github.
1 Installing LeftParen
You’ll need PLT Scheme v4.1.5 installed. More recent versions may work, but they are untested.
Make sure that mzscheme is in your path. You should be ready to go if you can do this:
% mzscheme |
Welcome to MzScheme v4.1.5... |
> |
Installing LeftParen is done automatically when you perform various PLaneT require commands. See Tutorials for examples of this. On my system, starting a LeftParen web app for the first time (with a freshly installed version of PLT Scheme), takes about an hour. (The time is spent downloading and installing a bunch of files into your local PLaneT repository.)
At any point, if you see an error like
make-directory: cannot make directory: /home/rob/.plt-scheme/planet/300/4.1.5 (Permission denied; errno=13) |
try re-running the command with sudo (you won’t need to do this everytime–just the once).
2 Tutorials
2.1 Hello, World
We’re going to make a project called hello-world. Change to the directory that you’d like to create the project in. Then issue
% mzscheme -e '(require (planet vegashacker/leftparen:5:=0/generate))' hello-world |
This will create a hello-world project directory for you. In this directory you’ll find the script directory, which contains some useful scripts. All paths are relative to this project directory, so when calling scripts, you always want to be at the project root.
% cd hello-world |
We need to make the scripts executable:
% chmod u+x script/server |
LeftParen has automatically generated everything we need to run our web app – we just need to start the server (again, you should be at the project root directory):
% ./script/server |
Populating caches... |
Done populating cache. |
Server is ready at http://localhost:8765/ (ctrl-c to stop it). |
Point your browser to http://localhost:8765 and you should see a familiar greeting:
Hello, World!
2.2 Blogerton the Blog
Now let’s try implementing the true "hello world" of web apps – a blog. First, execute the following commands from the directory in which you want to create your project:
% mzscheme -e '(require (planet vegashacker/leftparen:5:=0/generate))' blogerton |
% cd blogerton |
% chmod u+x script/server |
2.2.1 Changes to app.scm
We need to register a couple of pages in our app. The index-page was already set up for you, but you’ll need to add a page to create new posts, and one to view them. Make the define-app call look like this:
(define-app my-app |
(index-page (url "/")) |
(create-post-page (url "/post")) |
(view-post-page (url "/view/" (string-arg)))) |
2.2.2 Changes to main.scm
Now we need to define those pages that we declared in app.scm.
(define-page (index-page req) |
(** `(h1 "Blogerton") |
`(p ,(web-link "Create a new post" (page-url create-post-page))) |
`(ul ,@(map (lambda (p) `(li ,(paint-blog-post p))) |
(load-where '((type . blog-post)) |
#:sort-by 'created-at #:compare >))))) |
(define-page (create-post-page req) |
(form '((title "Title" text) (body "Body" long-text)) |
#:init '((type . blog-post)) |
#:on-done (lambda (post) (redirect-to-page view-post-page (rec-id post))))) |
(define-page (view-post-page req post-id) |
(paint-blog-post (load-rec post-id #:ensure '((type . blog-post))))) |
(define (paint-blog-post post) |
`(div (h2 ,(rec-prop post 'title)) |
(p ,(rec-prop post 'body)))) |
2.2.3 Launch Blogerton
You’re ready for launch. Start the server with
% ./script/server |
and you should have a basic blogging app, with persistent data, in 19 lines of code.
3 Reference
3.1 Forms
Most web applications make some use of web forms. The form function lets easily you get and process input from your users.
(form field-specs) → xexpr? |
field-specs : (listof field-spec?) |
field-spec? : (list symbol? string? field-type?) |
|
You create a form by listing, in order, "field specifications". For example, you might want a title field, followed by a description text box, followed by a photo upload field. Note that the many keyword arguments available to the form function aren’t documented yet.
Each field spec is of the form (field-name label field-type). For example, you create a title field, you might use the spec (title "Enter a title" text). The entire example metioned above might look like this:
(form '((title "Title" text) |
(description "Description" long-text) |
(photo "Your photo" image))) |
3.1.1 Image uploads
By default, uploaded images are stored in the uploaded-files directory in your project directory. You can customize this with the *PATH_TO_UPLOADED_FILES* setting. When images are saved, their original filenames are used with a 5-character code pre-pended to make filenames unique.
3.2 Sessions
A session is an object that allows you to easily store state about individual visitors to your web app. Sessions are stored on the server as a record with a virtually impossible-to-guess id. A cookie is left in the user’s web browser, which contains a pointer to a particular session id. These cookies expire one month after creation and, currently, this can’t be changed.
3.2.1 Creating sessions
|
This is an alternate to define-page, most commonly used in main.scm. The only difference is that after the request identifier, you must provide a session identifier. For example, to keep a counter (unique to each user), you could write:
(define-session-page (foo-page req sesh) |
(let ((c (session-get-val sesh 'counter 0))) |
(session-put-val! sesh 'counter (+ 1 c)) |
(number->string c))) |
When you define a session page, the session is automatically fetched for you (and created if necessary), and bound to the session identifier you provided.
3.2.2 Accessing sessions
(session-get-val session key [missing-val]) → any |
session : session |
key : symbol |
missing-val : any = #f |
(session-put-val! session key val) → session |
session : session |
key : symbol |
val : any |
3.3 Users
LeftParen provides built-in functionality for dealing with users, including registering users, logging users in and out, and storing persistent data about users. To get up-and-running quickly, you can use the high-level welcome-message function:
| |||||||||||||||||||||
session : session? | |||||||||||||||||||||
success-fn : (or/c (-> user? xexpr?) #f) = #f | |||||||||||||||||||||
no-register : boolean? = #f |
The function welcome-message produces a small area of text and links (commonly found in the top-right area of a web app). If the user is not currently logged in, login and register links are presented. If the user is logged in, a message welcoming them is displayed, along with a link to log out.
(current-user session) → (or/c user? #f) |
session : session? |
The function current-user returns the current user record, or #f if no user is available in the current session.
As an example, here is the complete page code for a web app that allows users to register, login and logout, and which prints a secret message if the user is logged in:
(define-session-page (index-page req sesh) |
(** (welcome-message sesh) |
(aif (current-user sesh) |
(format "The secret, ~A, is 42." (rec-prop it 'username)) |
"No secret for you."))) |
3.4 Feeds
You can create Atom or RSS feeds in your web app. A feed in LeftParen is just a page crafted in a paricular way. The core functions involved are atom-feed and rss-feed:
| ||||||||||||||||||||||||||||||||
→ response/full? | ||||||||||||||||||||||||||||||||
atom-feed-page : page? | ||||||||||||||||||||||||||||||||
feed-title : string? | ||||||||||||||||||||||||||||||||
updated-seconds : integer? | ||||||||||||||||||||||||||||||||
author-name : string? | ||||||||||||||||||||||||||||||||
feed-description : (or/c #f string?) = #f | ||||||||||||||||||||||||||||||||
feed-id : string? = THE_URL_OF_THE_GIVEN_ATOM_FEED_PAGE | ||||||||||||||||||||||||||||||||
related-content-link : string? = THE_LINK_TO_YOUR_WEB_APP | ||||||||||||||||||||||||||||||||
atom-items : (list-of atom-item?) = '() |
| ||||||||||||||||||||
→ response/full? | ||||||||||||||||||||
rss-feed-page : page? | ||||||||||||||||||||
feed-title : string? | ||||||||||||||||||||
feed-description : string? | ||||||||||||||||||||
related-content-link : string? = THE_LINK_TO_YOUR_WEB_APP | ||||||||||||||||||||
rss-items : (list-of rss-item?) = '() |
The #:items argument in each of these functions is a list of items constructed with atom-item and rss-item:
| ||||||||||||||||
→ atom-item? | ||||||||||||||||
title : string? | ||||||||||||||||
url : string? | ||||||||||||||||
updated-seconds : integer? | ||||||||||||||||
content : (or/c #f string?) = #f |
| |||||||||||||||||||||
title : string? | |||||||||||||||||||||
url : string? | |||||||||||||||||||||
content : (or/c #f string?) = #f |
Here’s an example Atom feed page:
(define-page (article-feed-page req) |
#:blank #t |
(atom-feed article-feed-page |
#:feed-title "LeftParen blog" |
#:feed-description "On LeftParen..." |
#:feed-updated/epoch-seconds (current-seconds) |
#:author-name "LP staffers" |
#:items (list |
(atom-item #:title "Status update..." |
#:url "http://blog.../50308696" |
#:updated-epoch-seconds |
(current-seconds) |
#:content "I’m nearing a...") |
(atom-item #:title "LeftParen 0.3..." |
#:updated-epoch-seconds |
(current-seconds) |
#:url "http://blog.../51814971" |
#:content "Tonight I...")))) |
Note that while using current-seconds for timestamps does satisfy the interface, it’s not really appropriate since these times are supposed to indicated freshness of the data. If basing your feed off of records, you might consider using created-when.
4 About/Acknowledgements
LeftParen was initially written by Rob Hunter. It makes use of Dispatch by Untyped, with-bindings by Jay McCarthy, and of course, PLT Scheme.
LeftParen used to use WEB by Jens Axel Soegaard and Instaservlet (now deprecated) by Untyped, which were both very helpful in getting the project started.
4.1 Code Contributors
Bill Hager
Joshua Herman