Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
290 lines (199 sloc) 13.7 KB
#lang scribble/doc
@(require scribble/manual)
@title{LeftParen 0.61 Documentation}
Website: @link[""]{}
LeftParen is a framework for quickly creating web apps. It runs on
PLT Scheme v4.1.5. LeftParen is released under an @link[""]{MIT License}. The source is available on @link[""]{github}.
@section{Installing LeftParen}
You'll need PLT Scheme v4.1.5 installed. More recent versions may work, but they are untested.
Make sure that @scheme[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 @link[""]{PLaneT} @scheme[require] commands. See @secref{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 @tt{sudo} (you won't need to do this everytime--just the once).
@section[#:tag "tutorials"]{Tutorials}
@subsection{Hello, World}
We're going to make a project called @tt{hello-world}. Change to the directory that you'd like to create the project in. Then issue
@verbatim{% mzscheme -e '(require (planet vegashacker/leftparen:5:=1/generate))' hello-world}
This will create a @tt{hello-world} project directory for you. In this directory you'll find the @tt{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.
@verbatim{% cd hello-world}
We need to make the scripts executable:
@verbatim{% 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 @link["http://localhost:8765"]{http://localhost:8765} and you should see a familiar greeting:
@tt{Hello, World!}
@subsection{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:=1/generate))' blogerton
% cd blogerton
% chmod u+x script/server
@subsubsection{Changes to @tt{app.scm}}
We need to register a couple of pages in our app. The @scheme[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 @scheme[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))))
@subsubsection{Changes to @tt{main.scm}}
Now we need to define those pages that we declared in @tt{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))))
@subsubsection{Launch Blogerton}
You're ready for launch. Start the server with
@verbatim{% ./script/server}
and you should have a basic blogging app, with persistent data, in 19 lines of code.
Most web applications make some use of web forms. The @scheme[form] function lets easily you get and process input from your users.
@defproc[(form (field-specs (listof field-spec?))) xexpr?]
@defthing[field-spec? (list symbol? string? field-type?)]
@defthing[field-type? (or/c 'text 'long-text 'number 'password 'image 'checkbox
'radio 'drop-down)]
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 @scheme[form] function aren't documented yet.
Each field spec is of the form @scheme[(field-name label field-type)]. For example, you create a title field, you might use the spec @scheme[(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)))
@subsubsection{Image uploads}
By default, uploaded images are stored in the @tt{uploaded-files} directory in your project directory. You can customize this with the @scheme[*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.
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.
@subsubsection{Creating sessions}
(page-name request-iden session-iden page-args ...)
body ...)]
This is an alternate to @scheme[define-page], most commonly used in @scheme[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.
@subsubsection{Accessing sessions}
@defproc[(session-get-val (session session) (key symbol) (missing-val any #f)) any]
@defproc[(session-put-val! (session session) (key symbol) (val any)) session]
@subsection{Storing closures in URLs}
LeftParen provides a simple way to create closures and attach them to a URL. Using this technique is often much simpler than the more traditional encoding of values in a URL by hand.
#:id body-as-url
(body-as-url (req-iden [key] [#:sticky]) body ...)
]{Creates a URL which, when visited, will execute the given @scheme[body]. If @scheme[key] is provided, then it will be used as the id to represent the closure. It's more common, however, to not pass your own key--in which case, a unique, random one will be chosen for you. @scheme[req-iden] will be bound to the request created when the link is executed and available for use in @scheme[body]. If @scheme[#:sticky] is provided, then the closure will be available for execution until the server is stopped. Otherwise, it is available for execution just once.
(define-page (index-page req)
(let ((x 0))
(web-link "Click to increment"
(body-as-url (req)
(set! x (+ x 1))
(format "x is ~A" x)))))
This creates a link, which, when clicked, prints @tt{x is 1}. If you reload the page, however, you'll see something like @tt{Expired or missing function 'q7c5ysd4935xhbhx2xbu'}. Changing the first sub-form passed to @scheme[body-as-url] to @scheme[(req #:sticky)] makes it so that successive hits to the link result in increasing values of @scheme[x].
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 @scheme[welcome-message] function:
@defproc[(welcome-message (session session?) (#:on-success success-fn (or/c (-> user? xexpr?) #f) #f) (#:no-register no-register boolean? #f)) xexpr]{The function @scheme[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.}
@defproc[(current-user (session session?)) (or/c user? #f)]{The function @scheme[current-user] returns the current user record, or @scheme[#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.")))
Note that @scheme[aif] is available in the @secref{utilities} included with LeftParen.
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 @scheme[atom-feed] and @scheme[rss-feed]:
@defproc[(atom-feed (atom-feed-page page?)
(#:feed-title feed-title string?)
(#:feed-updated/epoch-seconds updated-seconds integer?)
(#:author-name author-name string?)
(#:feed-description feed-description (or/c #f string?) #f)
(#:feed-id feed-id string? THE_URL_OF_THE_GIVEN_ATOM_FEED_PAGE)
(#:related-content-link related-content-link string? THE_LINK_TO_YOUR_WEB_APP)
(#:items atom-items (list-of atom-item?) '()))
@defproc[(rss-feed (rss-feed-page page?)
(#:feed-title feed-title string?)
(#:feed-description feed-description string?)
(#:related-content-link related-content-link string? THE_LINK_TO_YOUR_WEB_APP)
(#:items rss-items (list-of rss-item?) '()))
The @scheme[#:items] argument in each of these functions is a list of items constructed with @scheme[atom-item] and @scheme[rss-item]:
@defproc[(atom-item (#:title title string?)
(#:url url string?)
(#:updated-epoch-seconds updated-seconds integer?)
(#:content content (or/c #f string?) #f))
@defproc[(rss-item (#:title title string?)
(#:url url string?)
(#:content 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"
#:content "I’m nearing a...")
(atom-item #:title "LeftParen 0.3..."
#:url "http://blog.../51814971"
#:content "Tonight I..."))))
Note that while using @scheme[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 @scheme[created-when].
@section[#:tag "utilities"]{Utility Functions}
Some @link[""]{miscellaneous utility functions} are available along with LeftParen. To be able to use these functions, you'll need to require @tt{util.scm} at the top of your module. Something like this is common:
@schemeblock[(require (planet "leftparen.scm" ("vegashacker" "leftparen.plt" 5 (= 1)))
(planet "util.scm" ("vegashacker" "leftparen.plt" 5 (= 1))))]
LeftParen was initially written by @link[""]{Rob Hunter}. It makes use of @link[""]{Dispatch} by @link[""]{Untyped}, @link[""]{with-bindings} by @link[""]{Jay McCarthy}, and of course, @link[""]{PLT Scheme}.
LeftParen used to use @link[""]{WEB} by @link[""]{Jens Axel Soegaard} and @link[""]{Instaservlet} (now deprecated) by @link[""]{Untyped}, which were both very helpful in getting the project started.
@subsection{Code Contributors}
@item{Bill Hager}
@item{Joshua Herman}
Jump to Line
Something went wrong with that request. Please try again.