From 4ebebec089cf879b95a9e2366500226864ce98e2 Mon Sep 17 00:00:00 2001
From: Jay McCarthy
Date: Thu, 14 Aug 2008 22:27:57 +0000
Subject: [PATCH] Adding the tutorial.
svn: r11262
---
collects/web-server/info.ss | 6 +-
.../web-server/scribblings/managers.scrbl | 2 +-
.../tutorial/examples/htdocs/test-static.css | 15 +
.../tutorial/examples/iteration-1.ss | 39 +
.../tutorial/examples/iteration-10.ss | 137 +++
.../tutorial/examples/iteration-2.ss | 67 ++
.../tutorial/examples/iteration-3.ss | 60 ++
.../tutorial/examples/iteration-4.ss | 69 ++
.../tutorial/examples/iteration-5.ss | 134 +++
.../tutorial/examples/iteration-6.ss | 168 +++
.../tutorial/examples/iteration-7.ss | 168 +++
.../tutorial/examples/iteration-8.ss | 135 +++
.../tutorial/examples/iteration-9.ss | 137 +++
.../scribblings/tutorial/examples/model-2.ss | 58 +
.../scribblings/tutorial/examples/model-3.ss | 100 ++
.../scribblings/tutorial/examples/model.ss | 38 +
.../tutorial/examples/no-use-redirect.ss | 47 +
.../tutorial/examples/send-suspend-1.ss | 20 +
.../tutorial/examples/send-suspend-2.ss | 15 +
.../tutorial/examples/test-static.ss | 9 +
.../tutorial/examples/use-redirect.ss | 47 +
.../scribblings/tutorial/images/Flow1.dia | Bin 0 -> 1481 bytes
.../scribblings/tutorial/images/Flow1.png | Bin 0 -> 5250 bytes
.../scribblings/tutorial/images/Flow2.dia | Bin 0 -> 1606 bytes
.../scribblings/tutorial/images/Flow2.png | Bin 0 -> 6286 bytes
.../scribblings/tutorial/images/Flow3.dia | Bin 0 -> 1831 bytes
.../scribblings/tutorial/images/Flow3.png | Bin 0 -> 9085 bytes
.../web-server/scribblings/tutorial/sql.scrbl | 170 +++
.../scribblings/tutorial/tutorial-util.ss | 32 +
.../scribblings/tutorial/tutorial.scrbl | 996 ++++++++++++++++++
30 files changed, 2666 insertions(+), 3 deletions(-)
create mode 100644 collects/web-server/scribblings/tutorial/examples/htdocs/test-static.css
create mode 100644 collects/web-server/scribblings/tutorial/examples/iteration-1.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/iteration-10.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/iteration-2.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/iteration-3.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/iteration-4.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/iteration-5.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/iteration-6.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/iteration-7.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/iteration-8.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/iteration-9.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/model-2.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/model-3.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/model.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/no-use-redirect.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/send-suspend-1.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/send-suspend-2.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/test-static.ss
create mode 100644 collects/web-server/scribblings/tutorial/examples/use-redirect.ss
create mode 100644 collects/web-server/scribblings/tutorial/images/Flow1.dia
create mode 100644 collects/web-server/scribblings/tutorial/images/Flow1.png
create mode 100644 collects/web-server/scribblings/tutorial/images/Flow2.dia
create mode 100644 collects/web-server/scribblings/tutorial/images/Flow2.png
create mode 100644 collects/web-server/scribblings/tutorial/images/Flow3.dia
create mode 100644 collects/web-server/scribblings/tutorial/images/Flow3.png
create mode 100644 collects/web-server/scribblings/tutorial/sql.scrbl
create mode 100644 collects/web-server/scribblings/tutorial/tutorial-util.ss
create mode 100644 collects/web-server/scribblings/tutorial/tutorial.scrbl
diff --git a/collects/web-server/info.ss b/collects/web-server/info.ss
index a43e03b577..0bf155824c 100644
--- a/collects/web-server/info.ss
+++ b/collects/web-server/info.ss
@@ -1,8 +1,10 @@
#lang setup/infotab
-(define scribblings '(("scribblings/web-server.scrbl" (multi-page) (tool))))
+(define scribblings '(("scribblings/web-server.scrbl" (multi-page) (tool))
+ ("scribblings/tutorial/tutorial.scrbl" () (getting-started))))
(define mzscheme-launcher-libraries '("main.ss"))
(define mzscheme-launcher-names '("PLT Web Server"))
-(define compile-omit-paths '("default-web-root"))
+(define compile-omit-paths '("default-web-root"
+ "scribblings/tutorial/examples"))
diff --git a/collects/web-server/scribblings/managers.scrbl b/collects/web-server/scribblings/managers.scrbl
index 8fddc83edb..107d52ffa0 100644
--- a/collects/web-server/scribblings/managers.scrbl
+++ b/collects/web-server/scribblings/managers.scrbl
@@ -149,7 +149,7 @@ deployments of the @web-server .
The recommended usage of this manager is codified as the following function:
-@defproc[(create-threshold-LRU-manager
+@defproc[(make-threshold-LRU-manager
[instance-expiration-handler expiration-handler?]
[memory-threshold number?])
manager?]{
diff --git a/collects/web-server/scribblings/tutorial/examples/htdocs/test-static.css b/collects/web-server/scribblings/tutorial/examples/htdocs/test-static.css
new file mode 100644
index 0000000000..9767d57d2c
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/htdocs/test-static.css
@@ -0,0 +1,15 @@
+body {
+
+ margin-left: 10%;
+
+ margin-right: 10%;
+
+}
+
+p { font-family: sans-serif }
+
+h1 { color: green }
+
+h2 { font-size: small }
+
+span.hot { color: red }
\ No newline at end of file
diff --git a/collects/web-server/scribblings/tutorial/examples/iteration-1.ss b/collects/web-server/scribblings/tutorial/examples/iteration-1.ss
new file mode 100644
index 0000000000..76fa6d8630
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/iteration-1.ss
@@ -0,0 +1,39 @@
+#lang web-server/insta
+
+;; A blog is a (listof post)
+;; and a post is a (make-post title body)
+(define-struct post (title body))
+
+;; BLOG: blog
+;; The static blog.
+(define BLOG
+ (list (make-post "First Post" "This is my first post")
+ (make-post "Second Post" "This is another post")))
+
+;; start: request -> html-response
+;; Consumes a request, and produces a page that displays all of the
+;; web content.
+(define (start request)
+ (render-blog-page BLOG request))
+
+;; render-blog-page: blog request -> html-response
+;; Consumes a blog and a request, and produces an html-response page
+;; of the content of the blog.
+(define (render-blog-page a-blog request)
+ `(html (head (title "My Blog"))
+ (body (h1 "My Blog")
+ ,(render-posts a-blog))))
+
+;; render-post: post -> html-response
+;; Consumes a post, produces an html-response fragment of the post.
+(define (render-post a-post)
+ `(div ((class "post"))
+ ,(post-title a-post)
+ (p ,(post-body a-post))))
+
+;; render-posts: blog -> html-response
+;; Consumes a blog, produces an html-response fragment
+;; of all its posts.
+(define (render-posts a-blog)
+ `(div ((class "posts"))
+ ,@(map render-post a-blog)))
diff --git a/collects/web-server/scribblings/tutorial/examples/iteration-10.ss b/collects/web-server/scribblings/tutorial/examples/iteration-10.ss
new file mode 100644
index 0000000000..ed687bf176
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/iteration-10.ss
@@ -0,0 +1,137 @@
+#lang web-server/insta
+
+(require "model-3.ss")
+
+;; start: request -> html-response
+;; Consumes a request and produces a page that displays
+;; all of the web content.
+(define (start request)
+ (render-blog-page
+ (initialize-blog!
+ (build-path (find-system-path 'home-dir)
+ "the-blog-data.db"))
+ request))
+
+;; render-blog-page: blog request -> html-response
+;; Produces an html-response page of the content of the
+;; blog.
+(define (render-blog-page a-blog request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "My Blog"))
+ (body
+ (h1 "My Blog")
+ ,(render-posts a-blog make-url)
+ (form ((action
+ ,(make-url insert-post-handler)))
+ (input ((name "title")))
+ (input ((name "body")))
+ (input ((type "submit")))))))
+
+ (define (insert-post-handler request)
+ (define bindings (request-bindings request))
+ (blog-insert-post!
+ a-blog
+ (extract-binding/single 'title bindings)
+ (extract-binding/single 'body bindings))
+ (render-blog-page a-blog (redirect/get)))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-post-detail-page: post request -> html-response
+;; Consumes a post and produces a detail page of the post.
+;; The user will be able to either insert new comments
+;; or go back to render-blog-page.
+(define (render-post-detail-page a-blog a-post request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "Post Details"))
+ (body
+ (h1 "Post Details")
+ (h2 ,(post-title a-post))
+ (p ,(post-body a-post))
+ ,(render-as-itemized-list
+ (post-comments a-post))
+ (form ((action
+ ,(make-url insert-comment-handler)))
+ (input ((name "comment")))
+ (input ((type "submit"))))
+ (a ((href ,(make-url back-handler)))
+ "Back to the blog"))))
+
+ (define (parse-comment bindings)
+ (extract-binding/single 'comment bindings))
+
+ (define (insert-comment-handler request)
+ (render-confirm-add-comment-page
+ a-blog
+ (parse-comment (request-bindings request))
+ a-post
+ request))
+
+ (define (back-handler request)
+ (render-blog-page a-blog request))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-confirm-add-comment-page :
+;; blog comment post request -> html-response
+;; Consumes a comment that we intend to add to a post, as well
+;; as the request. If the user follows through, adds a comment
+;; and goes back to the display page. Otherwise, goes back to
+;; the detail page of the post.
+(define (render-confirm-add-comment-page a-blog a-comment
+ a-post request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "Add a Comment"))
+ (body
+ (h1 "Add a Comment")
+ "The comment: " (div (p ,a-comment))
+ "will be added to "
+ (div ,(post-title a-post))
+
+ (p (a ((href ,(make-url yes-handler)))
+ "Yes, add the comment."))
+ (p (a ((href ,(make-url cancel-handler)))
+ "No, I changed my mind!")))))
+
+ (define (yes-handler request)
+ (post-insert-comment! a-blog a-post a-comment)
+ (render-post-detail-page a-blog a-post (redirect/get)))
+
+ (define (cancel-handler request)
+ (render-post-detail-page a-blog a-post request))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-post: post (handler -> string) -> html-response
+;; Consumes a post, produces an html-response fragment of the post.
+;; The fragment contains a link to show a detailed view of the post.
+(define (render-post a-blog a-post make-url)
+ (local [(define (view-post-handler request)
+ (render-post-detail-page a-blog a-post request))]
+ `(div ((class "post"))
+ (a ((href ,(make-url view-post-handler)))
+ ,(post-title a-post))
+ (p ,(post-body a-post))
+ (div ,(number->string (length (post-comments a-post)))
+ " comment(s)"))))
+
+;; render-posts: blog (handler -> string) -> html-response
+;; Consumes a make-url, produces an html-response fragment
+;; of all its posts.
+(define (render-posts a-blog make-url)
+ (local [(define (render-post/make-url a-post)
+ (render-post a-blog a-post make-url))]
+ `(div ((class "posts"))
+ ,@(map render-post/make-url (blog-posts a-blog)))))
+
+;; render-as-itemized-list: (listof html-response) -> html-response
+;; Consumes a list of items, and produces a rendering as
+;; an unorderered list.
+(define (render-as-itemized-list fragments)
+ `(ul ,@(map render-as-item fragments)))
+
+;; render-as-item: html-response -> html-response
+;; Consumes an html-response, and produces a rendering
+;; as a list item.
+(define (render-as-item a-fragment)
+ `(li ,a-fragment))
\ No newline at end of file
diff --git a/collects/web-server/scribblings/tutorial/examples/iteration-2.ss b/collects/web-server/scribblings/tutorial/examples/iteration-2.ss
new file mode 100644
index 0000000000..e422354f5b
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/iteration-2.ss
@@ -0,0 +1,67 @@
+#lang web-server/insta
+
+;; A blog is a (listof post)
+;; and a post is a (make-post title body)
+(define-struct post (title body))
+
+;; BLOG: blog
+;; The static blog.
+(define BLOG
+ (list (make-post "First Post" "This is my first post")
+ (make-post "Second Post" "This is another post")))
+
+;; start: request -> html-response
+;; Consumes a request and produces a page that displays all of the
+;; web content.
+(define (start request)
+ (local [(define a-blog
+ (cond [(can-parse-post? (request-bindings request))
+ (cons (parse-post (request-bindings request))
+ BLOG)]
+ [else
+ BLOG]))]
+ (render-blog-page a-blog request)))
+
+
+;; can-parse-post?: bindings -> boolean
+;; Produces true if bindings contains values for 'title and 'body.
+(define (can-parse-post? bindings)
+ (and (exists-binding? 'title bindings)
+ (exists-binding? 'body bindings)))
+
+
+;; parse-post: bindings -> post
+;; Consuems a bindings, and produces a post out of the bindings.
+(define (parse-post bindings)
+ (make-post (extract-binding/single 'title bindings)
+ (extract-binding/single 'body bindings)))
+
+;; render-blog-page: blog request -> html-response
+;; Consumes a blog and a request, and produces an html-response page
+;; of the content of the blog.
+(define (render-blog-page a-blog request)
+ `(html (head (title "My Blog"))
+ (body
+ (h1 "My Blog")
+ ,(render-posts a-blog)
+ (form
+ (input ((name "title")))
+ (input ((name "body")))
+ (input ((type "submit")))))))
+
+
+
+;; render-post: post -> html-response
+;; Consumes a post, produces an html-response fragment of the post.
+(define (render-post a-post)
+ `(div ((class "post"))
+ ,(post-title a-post)
+ (p ,(post-body a-post))))
+
+
+;; render-posts: blog -> html-response
+;; Consumes a blog, produces an html-response fragment
+;; of all its posts.
+(define (render-posts a-blog)
+ `(div ((class "posts"))
+ ,@(map render-post a-blog)))
diff --git a/collects/web-server/scribblings/tutorial/examples/iteration-3.ss b/collects/web-server/scribblings/tutorial/examples/iteration-3.ss
new file mode 100644
index 0000000000..882f4c41e9
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/iteration-3.ss
@@ -0,0 +1,60 @@
+#lang web-server/insta
+
+;; A blog is a (listof post)
+;; and a post is a (make-post title body)
+(define-struct post (title body))
+
+;; BLOG: blog
+;; The static blog.
+(define BLOG
+ (list (make-post "First Post" "This is my first post")
+ (make-post "Second Post" "This is another post")))
+
+;; start: request -> html-response
+;; Consumes a request and produces a page that displays all of the
+;; web content.
+(define (start request)
+ (render-blog-page BLOG request))
+
+;; parse-post: bindings -> post
+;; Extracts a post out of the bindings.
+(define (parse-post bindings)
+ (make-post (extract-binding/single 'title bindings)
+ (extract-binding/single 'body bindings)))
+
+;; render-blog-page: blog request -> html-response
+;; Consumes a blog and a request, and produces an html-response page
+;; of the content of the blog.
+(define (render-blog-page a-blog request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "My Blog"))
+ (body
+ (h1 "My Blog")
+ ,(render-posts a-blog)
+ (form ((action
+ ,(make-url insert-post-handler)))
+ (input ((name "title")))
+ (input ((name "body")))
+ (input ((type "submit")))))))
+
+ (define (insert-post-handler request)
+ (render-blog-page
+ (cons (parse-post (request-bindings request))
+ a-blog)
+ request))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-post: post -> html-response
+;; Consumes a post, produces an html-response fragment of the post.
+(define (render-post a-post)
+ `(div ((class "post"))
+ ,(post-title a-post)
+ (p ,(post-body a-post))))
+
+;; render-posts: blog -> html-response
+;; Consumes a blog, produces an html-response fragment
+;; of all its posts.
+(define (render-posts a-blog)
+ `(div ((class "posts"))
+ ,@(map render-post a-blog)))
diff --git a/collects/web-server/scribblings/tutorial/examples/iteration-4.ss b/collects/web-server/scribblings/tutorial/examples/iteration-4.ss
new file mode 100644
index 0000000000..3a5a766a57
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/iteration-4.ss
@@ -0,0 +1,69 @@
+#lang web-server/insta
+
+;; A blog is a (make-blog posts)
+;; where posts is a (listof post)
+(define-struct blog (posts) #:mutable)
+
+;; and post is a (make-post title body)
+;; where title is a string, and body is a string
+(define-struct post (title body))
+
+;; BLOG: blog
+;; The initial BLOG.
+(define BLOG
+ (make-blog
+ (list (make-post "First Post" "This is my first post")
+ (make-post "Second Post" "This is another post"))))
+
+;; blog-insert-post!: blog post -> void
+;; Consumes a blog and a post, adds the post at the top of the blog.
+(define (blog-insert-post! a-blog a-post)
+ (set-blog-posts! a-blog
+ (cons a-post (blog-posts a-blog))))
+
+;; start: request -> html-response
+;; Consumes a request and produces a page that displays
+;; all of the web content.
+(define (start request)
+ (render-blog-page request))
+
+;; parse-post: bindings -> post
+;; Extracts a post out of the bindings.
+(define (parse-post bindings)
+ (make-post (extract-binding/single 'title bindings)
+ (extract-binding/single 'body bindings)))
+
+;; render-blog-page: request -> html-response
+;; Produces an html-response page of the content of the BLOG.
+(define (render-blog-page request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "My Blog"))
+ (body
+ (h1 "My Blog")
+ ,(render-posts)
+ (form ((action
+ ,(make-url insert-post-handler)))
+ (input ((name "title")))
+ (input ((name "body")))
+ (input ((type "submit")))))))
+
+ (define (insert-post-handler request)
+ (blog-insert-post!
+ BLOG (parse-post (request-bindings request)))
+ (render-blog-page request))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-post: post -> html-response
+;; Consumes a post, produces an html-response fragment of the post.
+(define (render-post a-post)
+ `(div ((class "post"))
+ ,(post-title a-post)
+ (p ,(post-body a-post))))
+
+;; render-posts: -> html-response
+;; Consumes a blog, produces an html-response fragment
+;; of all its posts.
+(define (render-posts)
+ `(div ((class "posts"))
+ ,@(map render-post (blog-posts BLOG))))
diff --git a/collects/web-server/scribblings/tutorial/examples/iteration-5.ss b/collects/web-server/scribblings/tutorial/examples/iteration-5.ss
new file mode 100644
index 0000000000..2e9a141594
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/iteration-5.ss
@@ -0,0 +1,134 @@
+#lang web-server/insta
+
+;; A blog is a (make-blog posts)
+;; where posts is a (listof post)
+(define-struct blog (posts) #:mutable)
+
+;; and post is a (make-post title body comments)
+;; where title is a string, body is a string,
+;; and comments is a (listof string)
+(define-struct post (title body comments) #:mutable)
+
+;; BLOG: blog
+;; The initial BLOG.
+(define BLOG
+ (make-blog
+ (list (make-post "First Post"
+ "This is my first post"
+ (list "First comment!"))
+ (make-post "Second Post"
+ "This is another post"
+ (list)))))
+
+;; blog-insert-post!: blog post -> void
+;; Consumes a blog and a post, adds the post at the top of the blog.
+(define (blog-insert-post! a-blog a-post)
+ (set-blog-posts! a-blog
+ (cons a-post (blog-posts a-blog))))
+
+
+;; post-insert-comment!: post string -> void
+;; Consumes a post and a comment string. As a side-efect,
+;; adds the comment to the bottom of the post's list of comments.
+(define (post-insert-comment! a-post a-comment)
+ (set-post-comments!
+ a-post
+ (append (post-comments a-post) (list a-comment))))
+
+;; start: request -> html-response
+;; Consumes a request, and produces a page that displays
+;; all of the web content.
+(define (start request)
+ (render-blog-page request))
+
+;; render-blog-page: request -> html-response
+;; Produces an html-response page of the content of the
+;; BLOG.
+(define (render-blog-page request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "My Blog"))
+ (body
+ (h1 "My Blog")
+ ,(render-posts make-url)
+ (form ((action
+ ,(make-url insert-post-handler)))
+ (input ((name "title")))
+ (input ((name "body")))
+ (input ((type "submit")))))))
+
+ ;; parse-post: bindings -> post
+ ;; Extracts a post out of the bindings.
+ (define (parse-post bindings)
+ (make-post (extract-binding/single 'title bindings)
+ (extract-binding/single 'body bindings)
+ (list)))
+
+ (define (insert-post-handler request)
+ (blog-insert-post!
+ BLOG (parse-post (request-bindings request)))
+ (render-blog-page request))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-post-detail-page: post request -> html-response
+;; Consumes a post and request, and produces a detail page
+;; of the post. The user will be able to insert new comments.
+(define (render-post-detail-page a-post request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "Post Details"))
+ (body
+ (h1 "Post Details")
+ (h2 ,(post-title a-post))
+ (p ,(post-body a-post))
+ ,(render-as-itemized-list
+ (post-comments a-post))
+ (form ((action
+ ,(make-url insert-comment-handler)))
+ (input ((name "comment")))
+ (input ((type "submit")))))))
+
+ (define (parse-comment bindings)
+ (extract-binding/single 'comment bindings))
+
+ (define (insert-comment-handler a-request)
+ (post-insert-comment!
+ a-post (parse-comment (request-bindings a-request)))
+ (render-post-detail-page a-post a-request))]
+
+
+ (send/suspend/dispatch response-generator)))
+
+
+;; render-post: post (handler -> string) -> html-response
+;; Consumes a post, produces an html-response fragment of the post.
+;; The fragment contains a link to show a detailed view of the post.
+(define (render-post a-post make-url)
+ (local [(define (view-post-handler request)
+ (render-post-detail-page a-post request))]
+ `(div ((class "post"))
+ (a ((href ,(make-url view-post-handler)))
+ ,(post-title a-post))
+ (p ,(post-body a-post))
+ (div ,(number->string (length (post-comments a-post)))
+ " comment(s)"))))
+
+;; render-posts: (handler -> string) -> html-response
+;; Consumes a make-url, and produces an html-response fragment
+;; of all its posts.
+(define (render-posts make-url)
+ (local [(define (render-post/make-url a-post)
+ (render-post a-post make-url))]
+ `(div ((class "posts"))
+ ,@(map render-post/make-url (blog-posts BLOG)))))
+
+;; render-as-itemized-list: (listof html-response) -> html-response
+;; Consumes a list of items, and produces a rendering as
+;; an unorderered list.
+(define (render-as-itemized-list fragments)
+ `(ul ,@(map render-as-item fragments)))
+
+;; render-as-item: html-response -> html-response
+;; Consumes an html-response, and produces a rendering
+;; as a list item.
+(define (render-as-item a-fragment)
+ `(li ,a-fragment))
\ No newline at end of file
diff --git a/collects/web-server/scribblings/tutorial/examples/iteration-6.ss b/collects/web-server/scribblings/tutorial/examples/iteration-6.ss
new file mode 100644
index 0000000000..94ce399474
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/iteration-6.ss
@@ -0,0 +1,168 @@
+#lang web-server/insta
+
+;; A blog is a (make-blog posts)
+;; where posts is a (listof post)
+(define-struct blog (posts) #:mutable)
+
+;; and post is a (make-post title body comments)
+;; where title is a string, body is a string,
+;; and comments is a (listof string)
+(define-struct post (title body comments) #:mutable)
+
+;; BLOG: blog
+;; The initial BLOG.
+(define BLOG
+ (make-blog
+ (list (make-post "First Post"
+ "This is my first post"
+ (list "First comment!"))
+ (make-post "Second Post"
+ "This is another post"
+ (list)))))
+
+;; blog-insert-post!: blog post -> void
+;; Consumes a blog and a post, adds the post at the top of the blog.
+(define (blog-insert-post! a-blog a-post)
+ (set-blog-posts! a-blog
+ (cons a-post (blog-posts a-blog))))
+
+
+;; post-insert-comment!: post string -> void
+;; Consumes a post and a comment string. As a side-efect,
+;; adds the comment to the bottom of the post's list of comments.
+(define (post-insert-comment! a-post a-comment)
+ (set-post-comments!
+ a-post
+ (append (post-comments a-post) (list a-comment))))
+
+;; start: request -> html-response
+;; Consumes a request and produces a page that displays
+;; all of the web content.
+(define (start request)
+ (render-blog-page request))
+
+;; render-blog-page: request -> html-response
+;; Produces an html-response page of the content of the
+;; BLOG.
+(define (render-blog-page request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "My Blog"))
+ (body
+ (h1 "My Blog")
+ ,(render-posts make-url)
+ (form ((action
+ ,(make-url insert-post-handler)))
+ (input ((name "title")))
+ (input ((name "body")))
+ (input ((type "submit")))))))
+
+ ;; parse-post: bindings -> post
+ ;; Extracts a post out of the bindings.
+ (define (parse-post bindings)
+ (make-post (extract-binding/single 'title bindings)
+ (extract-binding/single 'body bindings)
+ (list)))
+
+ (define (insert-post-handler request)
+ (blog-insert-post!
+ BLOG (parse-post (request-bindings request)))
+ (render-blog-page request))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-post-detail-page: post request -> html-response
+;; Consumes a post and produces a detail page of the post.
+;; The user will be able to either insert new comments
+;; or go back to render-blog-page.
+(define (render-post-detail-page a-post request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "Post Details"))
+ (body
+ (h1 "Post Details")
+ (h2 ,(post-title a-post))
+ (p ,(post-body a-post))
+ ,(render-as-itemized-list
+ (post-comments a-post))
+ (form ((action
+ ,(make-url insert-comment-handler)))
+ (input ((name "comment")))
+ (input ((type "submit"))))
+ (a ((href ,(make-url back-handler)))
+ "Back to the blog"))))
+
+ (define (parse-comment bindings)
+ (extract-binding/single 'comment bindings))
+
+ (define (insert-comment-handler request)
+ (render-confirm-add-comment-page
+ (parse-comment (request-bindings request))
+ a-post
+ request))
+
+ (define (back-handler request)
+ (render-blog-page request))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-confirm-add-comment-page :
+;; comment post request -> html-response
+;; Consumes a comment that we intend to add to a post, as well
+;; as the request. If the user follows through, adds a comment
+;; and goes back to the display page. Otherwise, goes back to
+;; the detail page of the post.
+(define (render-confirm-add-comment-page a-comment a-post request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "Add a Comment"))
+ (body
+ (h1 "Add a Comment")
+ "The comment: " (div (p ,a-comment))
+ "will be added to "
+ (div ,(post-title a-post))
+
+ (p (a ((href ,(make-url yes-handler)))
+ "Yes, add the comment."))
+ (p (a ((href ,(make-url cancel-handler)))
+ "No, I changed my mind!")))))
+
+ (define (yes-handler request)
+ (post-insert-comment! a-post a-comment)
+ (render-post-detail-page a-post request))
+
+ (define (cancel-handler request)
+ (render-post-detail-page a-post request))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-post: post (handler -> string) -> html-response
+;; Consumes a post, produces an html-response fragment of the post.
+;; The fragment contains a link to show a detailed view of the post.
+(define (render-post a-post make-url)
+ (local [(define (view-post-handler request)
+ (render-post-detail-page a-post request))]
+ `(div ((class "post"))
+ (a ((href ,(make-url view-post-handler)))
+ ,(post-title a-post))
+ (p ,(post-body a-post))
+ (div ,(number->string (length (post-comments a-post)))
+ " comment(s)"))))
+
+;; render-posts: (handler -> string) -> html-response
+;; Consumes a make-url, produces an html-response fragment
+;; of all its posts.
+(define (render-posts make-url)
+ (local [(define (render-post/make-url a-post)
+ (render-post a-post make-url))]
+ `(div ((class "posts"))
+ ,@(map render-post/make-url (blog-posts BLOG)))))
+
+;; render-as-itemized-list: (listof html-response) -> html-response
+;; Consumes a list of items, and produces a rendering as
+;; an unorderered list.
+(define (render-as-itemized-list fragments)
+ `(ul ,@(map render-as-item fragments)))
+
+;; render-as-item: html-response -> html-response
+;; Consumes an html-response, and produces a rendering
+;; as a list item.
+(define (render-as-item a-fragment)
+ `(li ,a-fragment))
\ No newline at end of file
diff --git a/collects/web-server/scribblings/tutorial/examples/iteration-7.ss b/collects/web-server/scribblings/tutorial/examples/iteration-7.ss
new file mode 100644
index 0000000000..4a3e0b9893
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/iteration-7.ss
@@ -0,0 +1,168 @@
+#lang web-server/insta
+
+;; A blog is a (make-blog posts)
+;; where posts is a (listof post)
+(define-struct blog (posts) #:mutable)
+
+;; and post is a (make-post title body comments)
+;; where title is a string, body is a string,
+;; and comments is a (listof string)
+(define-struct post (title body comments) #:mutable)
+
+;; BLOG: blog
+;; The initial BLOG.
+(define BLOG
+ (make-blog
+ (list (make-post "First Post"
+ "This is my first post"
+ (list "First comment!"))
+ (make-post "Second Post"
+ "This is another post"
+ (list)))))
+
+;; blog-insert-post!: blog post -> void
+;; Consumes a blog and a post, adds the post at the top of the blog.
+(define (blog-insert-post! a-blog a-post)
+ (set-blog-posts! a-blog
+ (cons a-post (blog-posts a-blog))))
+
+
+;; post-insert-comment!: post string -> void
+;; Consumes a post and a comment string. As a side-efect,
+;; adds the comment to the bottom of the post's list of comments.
+(define (post-insert-comment! a-post a-comment)
+ (set-post-comments!
+ a-post
+ (append (post-comments a-post) (list a-comment))))
+
+;; start: request -> html-response
+;; Consumes a request and produces a page that displays
+;; all of the web content.
+(define (start request)
+ (render-blog-page request))
+
+;; render-blog-page: request -> html-response
+;; Produces an html-response page of the content of the
+;; BLOG.
+(define (render-blog-page request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "My Blog"))
+ (body
+ (h1 "My Blog")
+ ,(render-posts make-url)
+ (form ((action
+ ,(make-url insert-post-handler)))
+ (input ((name "title")))
+ (input ((name "body")))
+ (input ((type "submit")))))))
+
+ ;; parse-post: bindings -> post
+ ;; Extracts a post out of the bindings.
+ (define (parse-post bindings)
+ (make-post (extract-binding/single 'title bindings)
+ (extract-binding/single 'body bindings)
+ (list)))
+
+ (define (insert-post-handler request)
+ (blog-insert-post!
+ BLOG (parse-post (request-bindings request)))
+ (render-blog-page (redirect/get)))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-post-detail-page: post request -> html-response
+;; Consumes a post and produces a detail page of the post.
+;; The user will be able to either insert new comments
+;; or go back to render-blog-page.
+(define (render-post-detail-page a-post request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "Post Details"))
+ (body
+ (h1 "Post Details")
+ (h2 ,(post-title a-post))
+ (p ,(post-body a-post))
+ ,(render-as-itemized-list
+ (post-comments a-post))
+ (form ((action
+ ,(make-url insert-comment-handler)))
+ (input ((name "comment")))
+ (input ((type "submit"))))
+ (a ((href ,(make-url back-handler)))
+ "Back to the blog"))))
+
+ (define (parse-comment bindings)
+ (extract-binding/single 'comment bindings))
+
+ (define (insert-comment-handler request)
+ (render-confirm-add-comment-page
+ (parse-comment (request-bindings request))
+ a-post
+ request))
+
+ (define (back-handler request)
+ (render-blog-page request))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-confirm-add-comment-page :
+;; comment post request -> html-response
+;; Consumes a comment that we intend to add to a post, as well
+;; as the request. If the user follows through, adds a comment
+;; and goes back to the display page. Otherwise, goes back to
+;; the detail page of the post.
+(define (render-confirm-add-comment-page a-comment a-post request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "Add a Comment"))
+ (body
+ (h1 "Add a Comment")
+ "The comment: " (div (p ,a-comment))
+ "will be added to "
+ (div ,(post-title a-post))
+
+ (p (a ((href ,(make-url yes-handler)))
+ "Yes, add the comment."))
+ (p (a ((href ,(make-url cancel-handler)))
+ "No, I changed my mind!")))))
+
+ (define (yes-handler request)
+ (post-insert-comment! a-post a-comment)
+ (render-post-detail-page a-post (redirect/get)))
+
+ (define (cancel-handler request)
+ (render-post-detail-page a-post request))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-post: post (handler -> string) -> html-response
+;; Consumes a post, produces an html-response fragment of the post.
+;; The fragment contains a link to show a detailed view of the post.
+(define (render-post a-post make-url)
+ (local [(define (view-post-handler request)
+ (render-post-detail-page a-post request))]
+ `(div ((class "post"))
+ (a ((href ,(make-url view-post-handler)))
+ ,(post-title a-post))
+ (p ,(post-body a-post))
+ (div ,(number->string (length (post-comments a-post)))
+ " comment(s)"))))
+
+;; render-posts: (handler -> string) -> html-response
+;; Consumes a make-url, produces an html-response fragment
+;; of all its posts.
+(define (render-posts make-url)
+ (local [(define (render-post/make-url a-post)
+ (render-post a-post make-url))]
+ `(div ((class "posts"))
+ ,@(map render-post/make-url (blog-posts BLOG)))))
+
+;; render-as-itemized-list: (listof html-response) -> html-response
+;; Consumes a list of items, and produces a rendering as
+;; an unorderered list.
+(define (render-as-itemized-list fragments)
+ `(ul ,@(map render-as-item fragments)))
+
+;; render-as-item: html-response -> html-response
+;; Consumes an html-response, and produces a rendering
+;; as a list item.
+(define (render-as-item a-fragment)
+ `(li ,a-fragment))
\ No newline at end of file
diff --git a/collects/web-server/scribblings/tutorial/examples/iteration-8.ss b/collects/web-server/scribblings/tutorial/examples/iteration-8.ss
new file mode 100644
index 0000000000..f51fb32fe7
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/iteration-8.ss
@@ -0,0 +1,135 @@
+#lang web-server/insta
+
+(require "model.ss")
+
+;; start: request -> html-response
+;; Consumes a request and produces a page that displays
+;; all of the web content.
+(define (start request)
+ (render-blog-page request))
+
+;; render-blog-page: request -> html-response
+;; Produces an html-response page of the content of the
+;; BLOG.
+(define (render-blog-page request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "My Blog"))
+ (body
+ (h1 "My Blog")
+ ,(render-posts make-url)
+ (form ((action
+ ,(make-url insert-post-handler)))
+ (input ((name "title")))
+ (input ((name "body")))
+ (input ((type "submit")))))))
+
+ ;; parse-post: bindings -> post
+ ;; Extracts a post out of the bindings.
+ (define (parse-post bindings)
+ (make-post (extract-binding/single 'title bindings)
+ (extract-binding/single 'body bindings)
+ (list)))
+
+ (define (insert-post-handler request)
+ (blog-insert-post!
+ BLOG (parse-post (request-bindings request)))
+ (render-blog-page (redirect/get)))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-post-detail-page: post request -> html-response
+;; Consumes a post and produces a detail page of the post.
+;; The user will be able to either insert new comments
+;; or go back to render-blog-page.
+(define (render-post-detail-page a-post request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "Post Details"))
+ (body
+ (h1 "Post Details")
+ (h2 ,(post-title a-post))
+ (p ,(post-body a-post))
+ ,(render-as-itemized-list
+ (post-comments a-post))
+ (form ((action
+ ,(make-url insert-comment-handler)))
+ (input ((name "comment")))
+ (input ((type "submit"))))
+ (a ((href ,(make-url back-handler)))
+ "Back to the blog"))))
+
+ (define (parse-comment bindings)
+ (extract-binding/single 'comment bindings))
+
+ (define (insert-comment-handler request)
+ (render-confirm-add-comment-page
+ (parse-comment (request-bindings request))
+ a-post
+ request))
+
+ (define (back-handler request)
+ (render-blog-page request))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-confirm-add-comment-page :
+;; comment post request -> html-response
+;; Consumes a comment that we intend to add to a post, as well
+;; as the request. If the user follows through, adds a comment
+;; and goes back to the display page. Otherwise, goes back to
+;; the detail page of the post.
+(define (render-confirm-add-comment-page a-comment a-post request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "Add a Comment"))
+ (body
+ (h1 "Add a Comment")
+ "The comment: " (div (p ,a-comment))
+ "will be added to "
+ (div ,(post-title a-post))
+
+ (p (a ((href ,(make-url yes-handler)))
+ "Yes, add the comment."))
+ (p (a ((href ,(make-url cancel-handler)))
+ "No, I changed my mind!")))))
+
+ (define (yes-handler request)
+ (post-insert-comment! a-post a-comment)
+ (render-post-detail-page a-post (redirect/get)))
+
+ (define (cancel-handler request)
+ (render-post-detail-page a-post request))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-post: post (handler -> string) -> html-response
+;; Consumes a post, produces an html-response fragment of the post.
+;; The fragment contains a link to show a detailed view of the post.
+(define (render-post a-post make-url)
+ (local [(define (view-post-handler request)
+ (render-post-detail-page a-post request))]
+ `(div ((class "post"))
+ (a ((href ,(make-url view-post-handler)))
+ ,(post-title a-post))
+ (p ,(post-body a-post))
+ (div ,(number->string (length (post-comments a-post)))
+ " comment(s)"))))
+
+;; render-posts: (handler -> string) -> html-response
+;; Consumes a make-url, produces an html-response fragment
+;; of all its posts.
+(define (render-posts make-url)
+ (local [(define (render-post/make-url a-post)
+ (render-post a-post make-url))]
+ `(div ((class "posts"))
+ ,@(map render-post/make-url (blog-posts BLOG)))))
+
+;; render-as-itemized-list: (listof html-response) -> html-response
+;; Consumes a list of items, and produces a rendering as
+;; an unorderered list.
+(define (render-as-itemized-list fragments)
+ `(ul ,@(map render-as-item fragments)))
+
+;; render-as-item: html-response -> html-response
+;; Consumes an html-response, and produces a rendering
+;; as a list item.
+(define (render-as-item a-fragment)
+ `(li ,a-fragment))
\ No newline at end of file
diff --git a/collects/web-server/scribblings/tutorial/examples/iteration-9.ss b/collects/web-server/scribblings/tutorial/examples/iteration-9.ss
new file mode 100644
index 0000000000..05f4b1e71a
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/iteration-9.ss
@@ -0,0 +1,137 @@
+#lang web-server/insta
+
+(require "model-2.ss")
+
+;; start: request -> html-response
+;; Consumes a request and produces a page that displays
+;; all of the web content.
+(define (start request)
+ (render-blog-page
+ (initialize-blog!
+ (build-path (find-system-path 'home-dir)
+ "the-blog-data.db"))
+ request))
+
+;; render-blog-page: blog request -> html-response
+;; Produces an html-response page of the content of the
+;; blog.
+(define (render-blog-page a-blog request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "My Blog"))
+ (body
+ (h1 "My Blog")
+ ,(render-posts a-blog make-url)
+ (form ((action
+ ,(make-url insert-post-handler)))
+ (input ((name "title")))
+ (input ((name "body")))
+ (input ((type "submit")))))))
+
+ (define (insert-post-handler request)
+ (define bindings (request-bindings request))
+ (blog-insert-post!
+ a-blog
+ (extract-binding/single 'title bindings)
+ (extract-binding/single 'body bindings))
+ (render-blog-page a-blog (redirect/get)))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-post-detail-page: post request -> html-response
+;; Consumes a post and produces a detail page of the post.
+;; The user will be able to either insert new comments
+;; or go back to render-blog-page.
+(define (render-post-detail-page a-blog a-post request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "Post Details"))
+ (body
+ (h1 "Post Details")
+ (h2 ,(post-title a-post))
+ (p ,(post-body a-post))
+ ,(render-as-itemized-list
+ (post-comments a-post))
+ (form ((action
+ ,(make-url insert-comment-handler)))
+ (input ((name "comment")))
+ (input ((type "submit"))))
+ (a ((href ,(make-url back-handler)))
+ "Back to the blog"))))
+
+ (define (parse-comment bindings)
+ (extract-binding/single 'comment bindings))
+
+ (define (insert-comment-handler request)
+ (render-confirm-add-comment-page
+ a-blog
+ (parse-comment (request-bindings request))
+ a-post
+ request))
+
+ (define (back-handler request)
+ (render-blog-page a-blog request))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-confirm-add-comment-page :
+;; blog comment post request -> html-response
+;; Consumes a comment that we intend to add to a post, as well
+;; as the request. If the user follows through, adds a comment
+;; and goes back to the display page. Otherwise, goes back to
+;; the detail page of the post.
+(define (render-confirm-add-comment-page a-blog a-comment
+ a-post request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "Add a Comment"))
+ (body
+ (h1 "Add a Comment")
+ "The comment: " (div (p ,a-comment))
+ "will be added to "
+ (div ,(post-title a-post))
+
+ (p (a ((href ,(make-url yes-handler)))
+ "Yes, add the comment."))
+ (p (a ((href ,(make-url cancel-handler)))
+ "No, I changed my mind!")))))
+
+ (define (yes-handler request)
+ (post-insert-comment! a-blog a-post a-comment)
+ (render-post-detail-page a-blog a-post (redirect/get)))
+
+ (define (cancel-handler request)
+ (render-post-detail-page a-blog a-post request))]
+
+ (send/suspend/dispatch response-generator)))
+
+;; render-post: post (handler -> string) -> html-response
+;; Consumes a post, produces an html-response fragment of the post.
+;; The fragment contains a link to show a detailed view of the post.
+(define (render-post a-blog a-post make-url)
+ (local [(define (view-post-handler request)
+ (render-post-detail-page a-blog a-post request))]
+ `(div ((class "post"))
+ (a ((href ,(make-url view-post-handler)))
+ ,(post-title a-post))
+ (p ,(post-body a-post))
+ (div ,(number->string (length (post-comments a-post)))
+ " comment(s)"))))
+
+;; render-posts: blog (handler -> string) -> html-response
+;; Consumes a make-url, produces an html-response fragment
+;; of all its posts.
+(define (render-posts a-blog make-url)
+ (local [(define (render-post/make-url a-post)
+ (render-post a-blog a-post make-url))]
+ `(div ((class "posts"))
+ ,@(map render-post/make-url (blog-posts a-blog)))))
+
+;; render-as-itemized-list: (listof html-response) -> html-response
+;; Consumes a list of items, and produces a rendering as
+;; an unorderered list.
+(define (render-as-itemized-list fragments)
+ `(ul ,@(map render-as-item fragments)))
+
+;; render-as-item: html-response -> html-response
+;; Consumes an html-response, and produces a rendering
+;; as a list item.
+(define (render-as-item a-fragment)
+ `(li ,a-fragment))
\ No newline at end of file
diff --git a/collects/web-server/scribblings/tutorial/examples/model-2.ss b/collects/web-server/scribblings/tutorial/examples/model-2.ss
new file mode 100644
index 0000000000..797abc8c47
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/model-2.ss
@@ -0,0 +1,58 @@
+#lang scheme
+
+;; A blog is a (make-blog home posts)
+;; where home is a string, posts is a (listof post)
+(define-struct blog (home posts) #:mutable #:prefab)
+
+;; and post is a (make-post blog title body comments)
+;; where title is a string, body is a string,
+;; and comments is a (listof string)
+(define-struct post (title body comments) #:mutable #:prefab)
+
+;; initialize-blog! : path? -> blog
+;; Reads a blog from a path, if not present, returns default
+(define (initialize-blog! home)
+ (define the-blog
+ (with-handlers
+ ([exn? (lambda (exn)
+ (make-blog
+ (path->string home)
+ (list (make-post "First Post"
+ "This is my first post"
+ (list "First comment!"))
+ (make-post "Second Post"
+ "This is another post"
+ (list)))))])
+ (with-input-from-file home
+ read)))
+ (set-blog-home! the-blog (path->string home))
+ the-blog)
+
+;; save-blog! : blog -> void
+;; Saves the contents of a blog to its home
+(define (save-blog! a-blog)
+ (with-output-to-file (blog-home a-blog)
+ (lambda () (write a-blog))
+ #:exists 'replace))
+
+;; blog-insert-post!: blog string string -> void
+;; Consumes a blog and a post, adds the post at the top of the blog.
+(define (blog-insert-post! a-blog title body)
+ (set-blog-posts!
+ a-blog
+ (cons (make-post title body empty) (blog-posts a-blog)))
+ (save-blog! a-blog))
+
+;; post-insert-comment!: blog post string -> void
+;; Consumes a blog, a post and a comment string. As a side-efect,
+;; adds the comment to the bottom of the post's list of comments.
+(define (post-insert-comment! a-blog a-post a-comment)
+ (set-post-comments!
+ a-post
+ (append (post-comments a-post) (list a-comment)))
+ (save-blog! a-blog))
+
+(provide blog? blog-posts
+ post? post-title post-body post-comments
+ initialize-blog!
+ blog-insert-post! post-insert-comment!)
diff --git a/collects/web-server/scribblings/tutorial/examples/model-3.ss b/collects/web-server/scribblings/tutorial/examples/model-3.ss
new file mode 100644
index 0000000000..3b8f96bcd6
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/model-3.ss
@@ -0,0 +1,100 @@
+#lang scheme
+(require (prefix-in sqlite: (planet jaymccarthy/sqlite:3/sqlite)))
+
+;; A blog is a (make-blog db)
+;; where db is an sqlite database handle
+(define-struct blog (db))
+
+;; A post is a (make-post blog id)
+;; where blog is a blog and id is an integer?
+(define-struct post (blog id))
+
+;; initialize-blog! : path? -> blog?
+;; Sets up a blog database (if it doesn't exist)
+(define (initialize-blog! home)
+ (define db (sqlite:open home))
+ (define the-blog (make-blog db))
+ (with-handlers ([exn? (lambda (exn) (void))])
+ (sqlite:exec/ignore
+ db
+ (string-append
+ "CREATE TABLE posts "
+ "(id INTEGER PRIMARY KEY,"
+ "title TEXT, body TEXT)"))
+ (blog-insert-post!
+ the-blog "First Post" "This is my first post")
+ (blog-insert-post!
+ the-blog "Second Post" "This is another post")
+ (sqlite:exec/ignore
+ db "CREATE TABLE comments (pid INTEGER, content TEXT)")
+ (post-insert-comment!
+ the-blog (first (blog-posts the-blog))
+ "First comment!"))
+ the-blog)
+
+;; blog-posts : blog -> (listof post?)
+;; Queries for the post ids
+(define (blog-posts a-blog)
+ (map (compose (lambda (n) (make-post a-blog n))
+ string->number
+ (lambda (v) (vector-ref v 0)))
+ (rest (sqlite:select
+ (blog-db a-blog)
+ "SELECT id FROM posts"))))
+
+;; post-title : post -> string?
+;; Queries for the title
+(define (post-title a-post)
+ (vector-ref
+ (second
+ (sqlite:select
+ (blog-db (post-blog a-post))
+ (format "SELECT title FROM posts WHERE id = '~a'"
+ (post-id a-post))))
+ 0))
+
+;; post-body : post -> string?
+;; Queries for the body
+(define (post-body p)
+ (vector-ref
+ (second
+ (sqlite:select
+ (blog-db (post-blog p))
+ (format "SELECT body FROM posts WHERE id = '~a'"
+ (post-id p))))
+ 0))
+
+;; post-comments : post -> (listof string?)
+;; Queries for the comments
+(define (post-comments p)
+ (with-handlers ([exn? (lambda _ empty)])
+ (map
+ (lambda (v) (vector-ref v 0))
+ (rest
+ (sqlite:select
+ (blog-db (post-blog p))
+ (format "SELECT content FROM comments WHERE pid = '~a'"
+ (post-id p)))))))
+
+;; blog-insert-post!: blog? string? string? -> void
+;; Consumes a blog and a post, adds the post at the top of the blog.
+(define (blog-insert-post! a-blog title body)
+ (sqlite:insert
+ (blog-db a-blog)
+ (format "INSERT INTO posts (title, body) VALUES ('~a', '~a')"
+ title body)))
+
+;; post-insert-comment!: blog? post string -> void
+;; Consumes a blog, a post and a comment string. As a side-efect,
+;; adds the comment to the bottom of the post's list of comments.
+(define (post-insert-comment! a-blog p a-comment)
+ (sqlite:insert
+ (blog-db a-blog)
+ (format
+ "INSERT INTO comments (pid, content) VALUES ('~a', '~a')"
+ (post-id p) a-comment)))
+
+(provide blog? blog-posts
+ post? post-title post-body post-comments
+ initialize-blog!
+ blog-insert-post! post-insert-comment!)
diff --git a/collects/web-server/scribblings/tutorial/examples/model.ss b/collects/web-server/scribblings/tutorial/examples/model.ss
new file mode 100644
index 0000000000..1bcc43ac0b
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/model.ss
@@ -0,0 +1,38 @@
+#lang scheme
+
+;; A blog is a (make-blog posts)
+;; where posts is a (listof post)
+(define-struct blog (posts) #:mutable)
+
+;; and post is a (make-post title body comments)
+;; where title is a string, body is a string,
+;; and comments is a (listof string)
+(define-struct post (title body comments) #:mutable)
+
+;; BLOG: blog
+;; The initial BLOG.
+(define BLOG
+ (make-blog
+ (list (make-post "First Post"
+ "This is my first post"
+ (list "First comment!"))
+ (make-post "Second Post"
+ "This is another post"
+ (list)))))
+
+;; blog-insert-post!: blog post -> void
+;; Consumes a blog and a post, adds the post at the top of the blog.
+(define (blog-insert-post! a-blog a-post)
+ (set-blog-posts!
+ a-blog
+ (cons a-post (blog-posts a-blog))))
+
+;; post-insert-comment!: post string -> void
+;; Consumes a post and a comment string. As a side-efect,
+;; adds the comment to the bottom of the post's list of comments.
+(define (post-insert-comment! a-post a-comment)
+ (set-post-comments!
+ a-post
+ (append (post-comments a-post) (list a-comment))))
+
+(provide (all-defined-out))
diff --git a/collects/web-server/scribblings/tutorial/examples/no-use-redirect.ss b/collects/web-server/scribblings/tutorial/examples/no-use-redirect.ss
new file mode 100644
index 0000000000..9661dbad93
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/no-use-redirect.ss
@@ -0,0 +1,47 @@
+#lang web-server/insta
+
+;; A roster is a (make-roster names)
+;; where names is a list of string.
+(define-struct roster (names) #:mutable)
+
+;; roster-add-name!: roster string -> void
+;; Given a roster and a name, adds the name
+;; to the end of the roster.
+(define (roster-add-name! a-roster a-name)
+ (set-roster-names! a-roster
+ (append (roster-names a-roster)
+ (list a-name))))
+
+(define ROSTER (make-roster '("kathi" "shriram" "dan")))
+
+;; start: request -> html-response
+(define (start request)
+ (show-roster request))
+
+;; show-roster: request -> html-response
+(define (show-roster request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "Roster"))
+ (body (h1 "Roster")
+ ,(render-as-itemized-list
+ (roster-names ROSTER))
+ (form ((action
+ ,(make-url add-name-handler)))
+ (input ((name "a-name")))
+ (input ((type "submit")))))))
+ (define (parse-name bindings)
+ (extract-binding/single 'a-name bindings))
+
+ (define (add-name-handler request)
+ (roster-add-name!
+ ROSTER (parse-name (request-bindings request)))
+ (show-roster request))]
+ (send/suspend/dispatch response-generator)))
+
+;; render-as-itemized-list: (listof html-response) -> html-response
+(define (render-as-itemized-list fragments)
+ `(ul ,@(map render-as-item fragments)))
+
+;; render-as-item: html-response -> html-response
+(define (render-as-item a-fragment)
+ `(li ,a-fragment))
\ No newline at end of file
diff --git a/collects/web-server/scribblings/tutorial/examples/send-suspend-1.ss b/collects/web-server/scribblings/tutorial/examples/send-suspend-1.ss
new file mode 100644
index 0000000000..b0a8cc9dd9
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/send-suspend-1.ss
@@ -0,0 +1,20 @@
+#lang web-server/insta
+
+;; start: request -> html-response
+(define (start request)
+ (send/suspend/dispatch
+ (lambda (make-url)
+ `(html
+ (body
+ (a ((href ,(make-url link-1))) "Link 1")
+ (a ((href ,(make-url link-2))) "Link 2"))))))
+
+
+;; link-1: request -> html-response
+(define (link-1 request)
+ "This is link-1")
+
+
+;; link-2: request -> html-response
+(define (link-2 request)
+ "This is link-2")
diff --git a/collects/web-server/scribblings/tutorial/examples/send-suspend-2.ss b/collects/web-server/scribblings/tutorial/examples/send-suspend-2.ss
new file mode 100644
index 0000000000..c1ae5e5471
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/send-suspend-2.ss
@@ -0,0 +1,15 @@
+#lang web-server/insta
+
+(define (start request)
+ (show-counter 0))
+
+;; show-counter: number -> html-response
+(define (show-counter n)
+ (send/suspend/dispatch
+ (lambda (make-url)
+ `(html (head (title "Counting example"))
+ (body
+ (a ((href ,(make-url
+ (lambda (request)
+ (show-counter (+ n 1))))))
+ ,(number->string n)))))))
\ No newline at end of file
diff --git a/collects/web-server/scribblings/tutorial/examples/test-static.ss b/collects/web-server/scribblings/tutorial/examples/test-static.ss
new file mode 100644
index 0000000000..f14384fe1a
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/test-static.ss
@@ -0,0 +1,9 @@
+#lang web-server/insta
+(define (start request)
+ '(html (head (title "Testing"))
+ (link ((rel "stylesheet")
+ (href "/test-static.css")
+ (type "text/css")))
+ (body (h1 "This is a header")
+ (p "This is " (span ((class "hot")) "hot") "."))))
+(static-files-path "htdocs")
\ No newline at end of file
diff --git a/collects/web-server/scribblings/tutorial/examples/use-redirect.ss b/collects/web-server/scribblings/tutorial/examples/use-redirect.ss
new file mode 100644
index 0000000000..d5780d700d
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/examples/use-redirect.ss
@@ -0,0 +1,47 @@
+#lang web-server/insta
+
+;; A roster is a (make-roster names)
+;; where names is a list of string.
+(define-struct roster (names) #:mutable)
+
+;; roster-add-name!: roster string -> void
+;; Given a roster and a name, adds the name
+;; to the end of the roster.
+(define (roster-add-name! a-roster a-name)
+ (set-roster-names! a-roster
+ (append (roster-names a-roster)
+ (list a-name))))
+
+(define ROSTER (make-roster '("kathi" "shriram" "dan")))
+
+;; start: request -> html-response
+(define (start request)
+ (show-roster request))
+
+;; show-roster: request -> html-response
+(define (show-roster request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "Roster"))
+ (body (h1 "Roster")
+ ,(render-as-itemized-list
+ (roster-names ROSTER))
+ (form ((action
+ ,(make-url add-name-handler)))
+ (input ((name "a-name")))
+ (input ((type "submit")))))))
+ (define (parse-name bindings)
+ (extract-binding/single 'a-name bindings))
+
+ (define (add-name-handler request)
+ (roster-add-name!
+ ROSTER (parse-name (request-bindings request)))
+ (show-roster (redirect/get)))]
+ (send/suspend/dispatch response-generator)))
+
+;; render-as-itemized-list: (listof html-response) -> html-response
+(define (render-as-itemized-list fragments)
+ `(ul ,@(map render-as-item fragments)))
+
+;; render-as-item: html-response -> html-response
+(define (render-as-item a-fragment)
+ `(li ,a-fragment))
\ No newline at end of file
diff --git a/collects/web-server/scribblings/tutorial/images/Flow1.dia b/collects/web-server/scribblings/tutorial/images/Flow1.dia
new file mode 100644
index 0000000000000000000000000000000000000000..e16536fda752bd180eb258671362bdc60c1941d1
GIT binary patch
literal 1481
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
literal 0
HcmV?d00001
diff --git a/collects/web-server/scribblings/tutorial/images/Flow2.dia b/collects/web-server/scribblings/tutorial/images/Flow2.dia
new file mode 100644
index 0000000000000000000000000000000000000000..eed3017cae616337a19eebb9c85c27eb0c92a99d
GIT binary patch
literal 1606
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
literal 0
HcmV?d00001
diff --git a/collects/web-server/scribblings/tutorial/images/Flow3.dia b/collects/web-server/scribblings/tutorial/images/Flow3.dia
new file mode 100644
index 0000000000000000000000000000000000000000..eb1275a3165b42909dfa5e6e9b2df4f42092a8df
GIT binary patch
literal 1831
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 . db?)]
+@defthing[sqlite:exec/ignore (db? string? . -> . void)]
+@defthing[sqlite:select (db? string? . -> . (listof vector?))]
+@defthing[sqlite:insert (db? string? . -> . integer?)]
+
+
+The first thing we should do is decide on the relational structure of our model. We will use the following tables:
+
+@verbatim{
+ CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT, body TEXT)
+ CREATE TABLE comments (pid INTEGER, content TEXT)
+}
+
+Each post will have an identifier, a title, and a body. This is the same as our old Scheme structure,
+except we've added the identifier. (Actually, there was always an identifier---the memory pointer---but now
+we have to make it explicit in the database.)
+
+Each comment is tied to a post by the post's identifier and has textual content. We could have chosen to
+serialize comments with @scheme[write] and add a new TEXT column to the posts table to store the value.
+By adding a new comments table, we are more in accord with the relational style.
+
+A @scheme[blog] structure will simply be a container for the database handle:
+
+@defstruct[blog ([db db?])]
+
+@bold{Exercise.} Write the @scheme[blog] structure definition. (It does not need to be mutable or serializable.)
+
+We can now write the code to initialize a @scheme[blog] structure:
+@schemeblock[
+@code:comment{initialize-blog! : path? -> blog?}
+@code:comment{Sets up a blog database (if it doesn't exist)}
+(define (initialize-blog! home)
+ (define db (sqlite:open home))
+ (define the-blog (make-blog db))
+ (with-handlers ([exn? (lambda (exn) (void))])
+ (sqlite:exec/ignore db
+ (string-append
+ "CREATE TABLE posts"
+ "(id INTEGER PRIMARY KEY,"
+ "title TEXT, body TEXT)"))
+ (blog-insert-post!
+ the-blog "First Post" "This is my first post")
+ (blog-insert-post!
+ the-blog "Second Post" "This is another post")
+ (sqlite:exec/ignore
+ db "CREATE TABLE comments (pid INTEGER, content TEXT)")
+ (post-insert-comment!
+ the-blog (first (blog-posts the-blog))
+ "First comment!"))
+ the-blog)
+]
+
+@scheme[sqlite:open] will create a database if one does not already exist at the @scheme[home] path. But, we still need
+to initialize the database with the table definitions and initial data.
+
+We used @scheme[blog-insert-post!] and @scheme[post-insert-comment!] to initialize the database. Let's see their implementation:
+
+@schemeblock[
+@code:comment{blog-insert-post!: blog? string? string? -> void}
+@code:comment{Consumes a blog and a post, adds the post at the top of the blog.}
+(define (blog-insert-post! a-blog title body)
+ (sqlite:insert
+ (blog-db a-blog)
+ (format "INSERT INTO posts (title, body) VALUES ('~a', '~a')"
+ title body)))
+
+@code:comment{post-insert-comment!: blog? post string -> void}
+@code:comment{Consumes a blog, a post and a comment string. As a side-effect,}
+@code:comment{adds the comment to the bottom of the post's list of comments.}
+(define (post-insert-comment! a-blog p a-comment)
+ (sqlite:insert
+ (blog-db a-blog)
+ (format
+ "INSERT INTO comments (pid, content) VALUES ('~a', '~a')"
+ (post-id p) a-comment)))
+]
+
+@bold{Exercise.} Find the security hole common to these two functions.
+
+@centerline{------------}
+
+A user could submit a post with a title like, @scheme{null', 'null') and INSERT INTO accounts (username, password) VALUES ('ur','hacked} and get our simple @scheme[sqlite:insert] to make two INSERTs instead of one.
+
+ This is called an SQL injection attack. It can be resolved by using
+ prepared statements that let SQLite do the proper quoting for us. Refer
+ to the SQLite package documentation for usage.
+
+@centerline{------------}
+
+In @scheme[post-insert-comment!], we used @scheme[post-id], but we have not yet defined the new @scheme[post] structure.
+It @emph{seems} like a @scheme[post] should be represented by an integer id, because the post table contains an integer as the identifying value.
+
+However, we cannot tell from this structure
+what blog this posts belongs to, and therefore, what database; so, we could not extract the title or body values,
+since we do not know what to query. Therefore, we should associate the blog with each post:
+
+@defstruct[post ([blog blog?] [id integer?])]
+
+@bold{Exercise.} Write the structure definition for posts.
+
+The only function that creates posts is @scheme[blog-posts]:
+
+@schemeblock[
+@code:comment{blog-posts : blog -> (listof post?)}
+@code:comment{Queries for the post ids}
+(define (blog-posts a-blog)
+ (map (compose (lambda (n) (make-post a-blog n))
+ string->number
+ (lambda (v) (vector-ref v 0)))
+ (rest (sqlite:select (blog-db a-blog)
+ "SELECT id FROM posts"))))
+]
+
+@scheme[sqlite:select] returns a list of vectors. The first element of the list is the name of the columns.
+Each vector has one element for each column. Each element is a string representation of the value.
+
+At this point we can write the functions that operate on posts:
+@schemeblock[
+@code:comment{post-title : post -> string?}
+@code:comment{Queries for the title}
+(define (post-title a-post)
+ (vector-ref
+ (second
+ (sqlite:select
+ (blog-db (post-blog a-post))
+ (format
+ "SELECT title FROM posts WHERE id = '~a'"
+ (post-id a-post))))
+ 0))
+]
+
+@bold{Exercise.} Write the definition of @scheme[post-body].
+
+@bold{Exercise.} Write the definition of @scheme[post-comments].
+(Hint: Use @scheme[blog-posts] as a template, not @scheme[post-title].)
+
+@centerline{------------}
+
+The only change that we need to make to the application is to require the new model. The interface is exactly the same!
+
+@centerline{------------}
+
+Our model is now:
+
+@external-file["model-3.ss"]
+
+And our application is:
+
+@schememod[
+web-server/insta
+
+(require "model-3.ss")
+
+....
+]
+
diff --git a/collects/web-server/scribblings/tutorial/tutorial-util.ss b/collects/web-server/scribblings/tutorial/tutorial-util.ss
new file mode 100644
index 0000000000..7b72b87dcd
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/tutorial-util.ss
@@ -0,0 +1,32 @@
+#lang scheme
+(require scribble/basic
+ (for-syntax scheme/port)
+ scheme/include
+ (except-in scribble/manual link))
+(provide external-file)
+
+; Copied from guide/scribblings/contracts-utils
+(require (for-syntax (only-in scribble/comment-reader [read-syntax comment-reader])))
+(define-for-syntax (comment-schememod-reader path port)
+ (let ([pb (peek-byte port)])
+ (if (eof-object? pb)
+ pb
+ (let ([m (regexp-match #rx"^#lang " port)])
+ (unless m
+ (raise-syntax-error 'comment-scheme-reader "expected a #lang to begin file ~s" path))
+ (let ([np (let-values ([(line col pos) (port-next-location port)])
+ (relocate-input-port port line 0 pos))])
+ (port-count-lines! np)
+ (let loop ([objects '()])
+ (let ([next (comment-reader path np)])
+ (cond
+ [(eof-object? next)
+ #`(schememod #,@(reverse objects))]
+ [else
+ (loop (cons next objects))]))))))))
+
+(define-syntax (external-file stx)
+ (syntax-case stx ()
+ [(_ filename)
+ #`(include/reader #,(format "examples/~a" (syntax-e #'filename))
+ comment-schememod-reader)]))
\ No newline at end of file
diff --git a/collects/web-server/scribblings/tutorial/tutorial.scrbl b/collects/web-server/scribblings/tutorial/tutorial.scrbl
new file mode 100644
index 0000000000..9f02c47460
--- /dev/null
+++ b/collects/web-server/scribblings/tutorial/tutorial.scrbl
@@ -0,0 +1,996 @@
+#lang scribble/doc
+@(require scribble/manual
+ (for-label scheme)
+ (for-label web-server/servlet)
+ "tutorial-util.ss")
+
+@title{@bold{Cont}: Web Applications in PLT Scheme}
+
+By Danny Yoo (dyoo at cs dot wpi dot edu) & Jay McCarthy (jay at cs dot byu dot edu)
+
+How do we make dynamic web applications? This tutorial will show how we
+can build web applications using PLT Scheme. As our working example,
+we'll build a simple web journal (a "blog"). We'll cover how to start
+up a web server, how to generate dynamic web content, and how to
+interact with the user.
+
+The target audience for this tutorial are students who've gone through
+the design and use of structures in How to Design Programs, as well as
+use of higher-order functions, local, and a minor bit of mutation.
+
+@section{Getting Started}
+
+Everything you needed in this tutorial is provided in @link["http://plt-scheme.org/"]{PLT Scheme}.
+We will be using the DrScheme Module language. Enter the following into the Definition window.
+
+@schememod[
+web-server/insta
+(define (start request)
+ '(html
+ (head (title "My Blog"))
+ (body (h1 "Under construction"))))
+]
+
+Press the @onscreen{Run} button. If a web browser comes up with an "Under
+Construction" page, then clap your hands with delight: you've built
+your first web application! It doesn't do much yet, but we will get
+there. Press the @onscreen{Stop} button to shut the server down for now.
+
+@section{The Application}
+
+We want to motivate this tutorial by showing how to develop a blog.
+Users should be able to create posts and add comments to
+any posts. We'll take an iterative approach, with one or two pitfalls
+along the way. The game plan, roughly, will be:
+
+@itemize[
+ @item{Show a static list of posts.}
+ @item{Allow a user to add new posts to the system.}
+ @item{Extend the model to let a user add comments to a post.}
+ @item{Allow all users to share the same set of posts.}
+ @item{Serialize our data structures to disk.}
+ ]
+
+By the end of this tutorial, we'll have a simple blogging application.
+
+@section{Basic Blog}
+@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-1)]
+
+We start by considering our data definitions. We want to represent a
+list of posts. Let's say that a post is:
+
+@schemeblock[(define-struct post (title body))]
+
+@(defstruct post ([title string?] [body string?]))
+
+@bold{Exercise.} Make a few examples of posts.
+
+A blog, then, will be a list of posts:
+
+@(defthing blog (listof post?))
+
+As a very simple example of a blog:
+
+@schemeblock[
+(define BLOG (list (make-post "First Post!"
+ "Hey, this is my first post!")))
+]
+
+Now that we have a sample blog structure, let's get our web
+application to show it.
+
+@section{Rendering HTML}
+@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-1)]
+
+When a web browser visits our application's URL, the browser
+constructs a request structure and sends it off to our web
+application. Our start function will consume requests and produce
+responses. One basic kind of response is to show an HTML page.
+
+@schemeblock[
+ (define html-response/c
+ (or/c string?
+ (or/c (cons/c symbol? (listof html-response/c))
+ (cons/c symbol?
+ (cons/c (listof (list/c symbol? string?))
+ (listof html-response/c))))))]
+
+For example:
+
+The HTML @tt{hello} is represented as @scheme["hello"].
+
+@tt{This is an example
} is
+
+@scheme['(p "This is an example")].
+
+@tt{Past} is
+
+@scheme['(a ((href "link.html")) "Past")].
+
+@tt{This is
another
example.
} is
+
+@scheme['(p "This is " (div ((class "emph")) "another") " example.")].
+
+We can produce these @scheme[html-response]s by using @scheme[cons] and @scheme[list] directly.
+Doing so, however, can be notationally heavy. Consider:
+
+@schemeblock[
+ (list 'html (list 'head (list 'title "Some title"))
+ (list 'body (list 'p "This is a simple static page.")))
+]
+
+vs:
+
+@schemeblock[
+ '(html (head (title "Some title"))
+ (body (p "This is a simple static page.")))
+]
+
+They both produce the same @scheme[html-response], but the latter is a lot
+easier to type and read. We've been using the extended list
+abbreviation form described in Section 13 of How to Design Programs:
+by using a leading forward quote mark to concisely represent the list
+structure, we can construct static html responses with aplomb.
+
+However, we can run into a problem when we use simple list
+abbreviation with dynamic content. If we have expressions to inject
+into the @scheme[html-response] structure, we can't use a simple list-abbreviation
+approach because those expressions will be treated literally as part
+of the list structure!
+
+We want a notation that gives us the convenience of quoted list
+abbreviations, but with the option to treat a portion of the structure
+as a normal expression. That is, we would like to define a template
+whose placeholders can be easily expressed and filled in dynamically.
+
+Scheme provides this templating functionality with quasiquotation.
+Quasiquotation uses a leading back-quote in front of the whole
+structure. Like regular quoted list abbreviation, the majority of the
+list structure will be literally preserved in the nested list result.
+In places where we'd like a subexpression's value to be plugged in, we
+prepend an unquoting comma in front of the subexpression. As an
+example:
+
+@schemeblock[
+@code:comment{render-greeting: string -> html-response}
+@code:comment{Consumes a name, and produces a dynamic html-response.}
+(define (render-greeting a-name)
+ `(html (head (title "Welcome"))
+ (body (p ,(string-append "Hello " a-name)))))
+]
+
+@bold{Exercise.} Write a function that consumes a @scheme[post] and produces
+an @scheme[html-response] representing that content.
+
+@defthing[render-post (post? . -> . html-response/c)]
+
+As an example, we want:
+
+@schemeblock[
+ (render-post (make-post "First post!" "This is a first post."))
+]
+
+to produce:
+
+@schemeblock[
+ '(div ((class "post")) "First post!" (p "This is a first post."))
+]
+
+@bold{Exercise.} Revise @scheme[render-post] to show the number of comments attached
+to a post.
+
+@centerline{------------}
+
+If an expression produces a list of @scheme[html-response] fragments, we may
+want to splice in the elements of a list into our template, rather
+plug in the whole list itself. In these situations, we can use the
+splicing form @scheme[,@expression].
+
+As an example, we may want a helper function that transforms a
+@scheme[html-response] list into a fragment representing an unordered, itemized
+HTML list:
+
+@schemeblock[
+@code:comment{render-as-itemized-list: (listof html-response) -> html-response}
+@code:comment{Consumes a list of items, and produces a rendering}
+@code:comment{as an unordered list.}
+(define (render-as-itemized-list fragments)
+ `(ul ,@(map render-as-item fragments)))
+
+@code:comment{render-as-item: html-response -> html-response}
+@code:comment{Consumes an html-response, and produces a rendering}
+@code:comment{as a list item.}
+(define (render-as-item a-fragment)
+ `(li ,a-fragment))
+]
+
+@bold{Exercise.} Write a function @scheme[render-posts] that consumes a @scheme[(listof post?)]
+and produces an @scheme[html-response] for that content.
+
+@defthing[render-posts ((listof post?) . -> . html-response/c)]
+
+As examples:
+
+@schemeblock[
+(render-posts empty)
+]
+
+should produce:
+
+@schemeblock[
+'(div ((class "posts")))
+]
+
+@schemeblock[
+(render-posts (list (make-post "Post 1" "Body 1")
+ (make-post "Post 2" "Body 2")))
+]
+
+should produce:
+
+@schemeblock[
+'(div ((class "posts"))
+ (div ((class "post")) "Post 1" "Body 1")
+ (div ((class "post")) "Post 2" "Body 2"))
+]
+
+@centerline{------------}
+
+Now that we have the @scheme[render-posts] function handy, let's revisit our
+web application and change our @scheme[start] function to return an interesting
+@scheme[html-response].
+
+@external-file["iteration-1.ss"]
+
+If we press Run, we should see the blog posts in our web browser.
+
+@section{Inspecting Requests}
+@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-2
+ web-server/servlet)]
+
+Our application still seems a bit static: although we're building the
+page dynamically, we haven't yet provided a way for an external user
+to add new posts. Let's tackle that now. Let's provide a form that
+will let the user add a new blog entry. When the user presses the
+submit button, we want the user to see the new post at the top of the
+page.
+
+Until now, we've been passing around a @scheme[request] object without doing
+anything with it. As we might expect, the @scheme[request] object isn't meant
+to be ignored so much! When a user fills out a web form and submits
+it, that user's browser constructs a new @scheme[request] that holds the form
+values in it. We can use the function @scheme[request-bindings] to grab at
+the values that the user has filled out. The type of @scheme[request-bindings]
+is:
+
+@defthing[request-bindings (request? . -> . bindings?)]
+
+Along with @scheme[request-bindings], there's another function called
+@scheme[extract-binding/single] that takes this as well as a name, and returns
+the value associated to that name.
+
+@defthing[extract-binding/single (symbol? bindings? . -> . string?)]
+
+Finally, we can check to see if a name exists in a binding with
+@scheme[exists-binding?]:
+
+@defthing[exists-binding? (symbol? bindings? . -> . boolean?)]
+
+With these functions, we can design functions that consume @scheme[request]s
+and do something useful.
+
+@bold{Exercise.} Write a function @scheme[can-parse-post?] that consumes a @scheme[bindings?].
+It should produce @scheme[#t] if there exist bindings both for the symbols
+@scheme['title] and @scheme['body], and @scheme[#f] otherwise.
+
+@defthing[can-parse-post? (bindings? . -> . boolean?)]
+
+@bold{Exercise.} Write a function @scheme[parse-post] that consumes a bindings.
+Assume that the bindings structure has values for the symbols @scheme['title]
+and @scheme['body]. @scheme[parse-post] should produce a post containing those values.
+
+@defthing[parse-post (bindings? . -> . post?)]
+
+Now that we have these helper functions, we can extend our web
+application to handle form input. We'll add a small form at the
+bottom, and adjust out program to handle the addition of new posts.
+Our start method, then, will first see if the request has a parsable
+post, extend its set of posts if it can, and finally display those
+blog posts.
+
+@external-file["iteration-2.ss"]
+
+This appears to work... but there's an issue with this! Try to add
+two new posts. What happens?
+
+@section{Advanced Control Flow}
+@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-3)]
+
+For the moment, let's ignore the admittedly huge problem of having a
+blog that only accepts one new blog entry. Don't worry! We will fix
+this.
+
+But there's a higher-level problem with our program: although we do
+have a function, @scheme[start], that can respond to requests directed at our
+application's URL, that @scheme[start] function is starting to get overloaded
+with a lot of responsibility. Conceptually, @scheme[start] is now handling
+two different kinds of requests: it's either a request for showing a
+blog, or a request for adding a new blog post.
+
+What's happening is that @scheme[start] is becoming a traffic cop --- a
+dispatcher --- for all the behavior of our web application. As far as
+we know, if we want to add more functionality to our application,
+start needs to know how to deal. Can we can get different kinds of
+requests to automatically direct themselves to different functions?
+
+The web server library provides a function, @scheme[send/suspend/dispatch],
+that allows us to create URLs that direct to different parts of our
+application. Let's demonstrate a dizzying example. In a new file,
+enter the following in the definition window.
+
+@schememod[
+web-server/insta
+@code:comment{start: request -> html-response}
+(define (start request)
+ (phase-1 request))
+
+@code:comment{phase-1: request -> html-response}
+(define (phase-1 request)
+ (local [(define (response-generator make-url)
+ `(html
+ (body (h1 "Phase 1")
+ (a ((href ,(make-url phase-2)))
+ "click me!"))))]
+ (send/suspend/dispatch response-generator)))
+
+@code:comment{phase-2: request -> html-response}
+(define (phase-2 request)
+ (local [(define (response-generator make-url)
+ `(html
+ (body (h1 "Phase 2")
+ (a ((href ,(make-url phase-1)))
+ "click me!"))))]
+ (send/suspend/dispatch response-generator)))
+]
+
+This is a web application that goes round and round. When a user
+first visits the application, the user starts off in @scheme[phase-1]. The
+page that's generated has a hyperlink that, when clicked, continues to
+@scheme[phase-2]. The user can click back, and falls back to @scheme[phase-1], and the
+cycle repeats.
+
+Let's look more closely at the @scheme[send/suspend/dispatch] mechanism.
+@scheme[send/suspend/dispatch] consumes a response-generating function, and it
+gives that response-generator a function called @scheme[make-url] that we'll
+use to build special URLs. What makes these URLs special is this:
+when a web browser visits these URLs, our web application restarts,
+but not from start, but from the handler that we associate to the URL.
+In @scheme[phase-1], the use of @scheme[make-url] associates the link with @scheme[phase-2], and
+vice versa.
+
+We can be more sophisticated about the handlers associated with
+@scheme[make-url]. Because the handler is just a request-consuming function,
+it can be defined within a @scheme[local]. Consequently, a local-defined
+handler knows about all the variables that are in the scope of its
+definition. Here's another loopy example:
+
+@schememod[
+web-server/insta
+@code:comment{start: request -> html-response}
+(define (start request)
+ (show-counter 0 request))
+
+@code:comment{show-counter: number request -> html-response}
+@code:comment{Displays a number that's hyperlinked: when the link is pressed,}
+@code:comment{returns a new page with the incremented number.}
+(define (show-counter n request)
+ (local [(define (response-generator make-url)
+ `(html (head (title "Counting example"))
+ (body
+ (a ((href ,(make-url next-number-handler)))
+ ,(number->string n)))))
+
+ (define (next-number-handler request)
+ (show-counter (+ n 1) request))]
+
+ (send/suspend/dispatch response-generator)))
+]
+
+This example shows that we can accumulate the results of an
+interaction. Even though the user start off by visiting and seeing
+zero, the handlers produced by next-number-handler continue the
+interaction, accumulating a larger and larger number.
+
+Now that we've been going a little bit in circles, let's move forward
+back to the blog application. We will adjust the form's action so it
+directs to a URL that's associated to a separate handler.
+
+@external-file["iteration-3.ss"]
+
+Note that the structure of the @scheme[render-blog-page] function looks very
+similar to that of our last @scheme[show-counter] example. The user can
+finally add and see multiple posts to their blog.
+
+Unfortunately, there's still a problem. To see the problem: add a few
+posts to the system, and then open up a new browser window. In the
+new browser window, visit the web application's URL. What happens?
+
+@section{Share and Share Alike}
+@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-4)]
+
+We have run into another flaw with our application: each browser
+window keeps track of its own distinct blog. That defeats the point a
+blog for most people, that is, to share with others! When we insert a
+new post, rather than create a new blog value, we'd like to make a
+structural change to the existing blog. (HTDP Chapter 41). So
+let's add mutation into the mix.
+
+There's one small detail we need to touch: in the web-server language,
+structures are immutable by default. We'll want to override this
+default and get get access to the structure mutators. To do so, we
+adjust our structure definitions with the @scheme[#:mutable] keyword.
+
+Earlier, we had said that a @scheme[blog] was a list of @scheme[post]s,
+but because we want to allow the blog to be changed, let's revisit our
+definition so that a blog is a mutable structure:
+
+@schemeblock[(define-struct blog (posts) #:mutable)]
+
+@defstruct[blog ([posts (listof post?)])]
+
+Mutable structures provide functions to change the fields of a
+structure; in this case, we now have a structure mutator called
+@scheme[set-blog-posts!],
+
+@defthing[set-blog-posts! (blog? (listof post?) . -> . void)]
+
+and this will allow us to change the posts of a blog.
+
+@bold{Exercise.} Write a function @scheme[blog-insert-post!]
+
+@defthing[blog-insert-post! (blog? post? . -> . void)]
+
+The intended side effect of the function will be to extend the blog's
+posts.
+
+@centerline{------------}
+
+Since we've changed the data representation of a blog, we'll need to
+revise our web application to use the updated representation. One
+other thing to note is that, within the web application, because we're
+sharing the same blog value, we don't need to pass it around with our
+handlers anymore: we can get at the current blog through our @scheme[BLOG]
+variable.
+
+After doing the adjustments incorporating @scheme[insert-blog-post!], and doing
+a little variable cleanup, our web application now looks like this:
+
+@external-file["iteration-4.ss"]
+
+Open two windows that visit our web application, and start adding in
+posts from both windows. We should see that both browsers are sharing
+the same blog.
+
+@section{Extending the Model}
+@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-5)]
+
+Next, let's extend the application so that each post can hold a list
+of comments. We refine the data definition of a blog to be:
+
+@defstruct[post ([title string?] [body string?] [comments (listof string?)]) #:mutable]
+
+@bold{Exercise.} Write the updated data structure definition for posts. Make
+sure to make the structure mutable, since we intend to add comments to
+posts.
+
+@bold{Exercise.} Make up a few examples of posts.
+
+@bold{Exercise.} Define a function @scheme[post-add-comment!]
+
+@defthing[post-add-comment! (post? string? . -> . void)]
+
+whose intended side effect is to add a new comment to the end of the post's
+list of comments.
+
+@bold{Exercise.} Adjust @scheme[render-post] so that the produced fragment will include the
+comments in an itemized list.
+
+@bold{Exercise.} Because we've extended a post to include comments, other
+post-manipulating parts of the application may need to be adjusted,
+such as uses of @scheme[make-post]. Identify and fix any other part of the
+application that needs to accommodate the post's new structure.
+
+@centerline{------------}
+
+Once we've changed the data structure of the posts and adjusted our
+functions to deal with this revised structure, the web application
+should be runnable. The user may even may even see some of the fruits
+of our labor: if the initial @scheme[BLOG] has a post with a comment, the user
+should see those comments now. But obviously, there's something
+missing: the user doesn't have the user interface to add comments to a
+post!
+
+@section{Breaking Up the Display}
+@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-5)]
+
+How should we incorporate comments more fully into the user's web
+experience? Seeing all the posts and comments all on one page may
+be a bit overwhelming. Perhaps we should hold off on showing the
+comments on the main blog page. Let's present a secondary "detail"
+view of a post, and present the comments there.
+
+The top-level view of a blog then can show the blog's title and body.
+We can also show a count of how many comments are associated to the
+post.
+
+So now we need some way to visit a post's detail page. One way to do
+this is to hyperlink each post's title: if the user wants to see the
+detail page of a post, user should be able to click the title to get
+there. From that post's detail page, we can even add a form to let
+the user add new comments.
+
+Here's a diagram of a simple page flow of our web application that
+should let us add comments.
+
+@image{scribblings/tutorial/images/Flow1.png}
+
+Each point in the diagram corresponds to a request-consuming handler.
+As we might suspect, we'll be using @scheme[send/suspend/dispatch] some more.
+Every arrow in the diagram will be realized as a URL that we generate
+with @scheme[make-url].
+
+This has a slightly messy consequence: previously, we've been
+rendering the list of posts without any hyperlinks. But since any
+function that generates a special dispatching URL uses make-url to do
+it, we'll need to adjust @scheme[render-posts] and @scheme[render-post] to consume and
+use @scheme[make-url] itself when it makes those hyperlinked titles.
+
+Our web application now looks like:
+
+@external-file["iteration-5.ss"]
+
+We now have an application that's pretty sophisticated: we can add
+posts and write comments. Still, there's a problem with this: once
+the user's in a @scheme[post-detail-page], they can't get back to the blog
+without pressing the browser's back button! That's disruptive. We
+should provide a page flow that lets us get back to the main
+blog-viewing page, to keep the user from every getting "stuck" in a
+dark corner of the web application.
+
+@section{Adding a Back Button}
+@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-6)]
+
+Here's a diagram of a our revised page flow of our web application.
+Maybe we can just add a BACK link from the @scheme[render-post-detail-page]
+that gets us back to viewing the top-level blog.
+
+@image{scribblings/tutorial/images/Flow2.png}
+
+@bold{Exercise.} Adjust @scheme[render-post-detail-page] to include another link that goes
+back to @scheme[render-blog-page].
+
+To make this more interesting, maybe we should enrich the flow a
+bit more. We can give the user a choice right before committing to
+their comment. Who knows? They may have a change of heart.
+
+@image{scribblings/tutorial/images/Flow3.png}
+
+Although this seems complicated, the shape of our handlers will look
+more-or-less like what we had before. After we've added all the
+handlers, our web application is fairly functional.
+
+@external-file["iteration-6.ss"]
+
+@section{Decorating With Style!}
+@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-7
+ web-server/insta/insta)]
+
+We have an application that's functionally complete, but is visual
+lacking. Let's try to improve its appearance. One way we can do this
+is to use a cascading style sheet. A style sheet can visual panache
+to our web pages. For example, if we'd like to turn all of our
+paragraphs green, we might add the following style declaration within
+our response.
+
+@scheme['(style ((type "text/css")) "p { color: green }")]
+
+It's tempting to directly embed this style information into our
+@scheme[html-response]s. However, our source file is already quite busy. We
+often want to separate the logical representation of our application
+from its presentation. Rather than directly embed the .css in the
+HTML response, let's instead add a link reference to an separate .css
+file.
+
+Until now, all the content that our web application has produced has
+come from a response generating handler. Of course, we know that not
+everything needs to be dynamically generated: it's common to have
+files that won't be changing. We should be able to serve these static
+resources (images, documents, .css files) alongside our web
+applications.
+
+To do this, we set aside a path to store these files, and then tell
+the web server where that directory is. The function
+@scheme[static-files-path],
+
+@defthing[static-files-path (path? -> void)]
+
+tells the web server to look in the given path when it receives a URL
+that looks like a static resource request.
+
+@bold{Exercise.} Create a simple web application called @filepath{test-static.ss} with the
+following content:
+
+@schememod[
+web-server/insta
+(define (start request)
+ '(html (head (title "Testing"))
+ (link ((rel "stylesheet")
+ (href "/test-static.css")
+ (type "text/css")))
+ (body (h1 "Testing")
+ (h2 "This is a header")
+ (p "This is " (span ((class "hot")) "hot") "."))))
+
+(static-files-path "htdocs")
+]
+
+Make a subdirectory called @filepath{htdocs} rooted in the same directory as
+the @filepath{test-static.ss} source. Finally, just to see that we can serve
+this .css page, create a very simple .css file @filepath{test-static.css} file
+in @filepath{htdocs/} with the following content:
+
+@verbatim{
+body {
+ margin-left: 10%;
+ margin-right: 10%;
+}
+p { font-family: sans-serif }
+h1 { color: green }
+h2 { font-size: small }
+span.hot { color: red }
+}
+
+At this point, run the application and look at the browser's output.
+We should see a Spartan web page, but it should still have some color
+in its cheeks.
+
+@centerline{------------}
+
+@bold{Exercise.}
+Improve the presentation of the blog web application by writing your
+an external style sheet to your tastes. Adjust all of the HTML
+response handlers to include a link to the style sheet.
+
+@section{The Double Submit Bug}
+@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-7
+ web-server/servlet)]
+
+There's yet another a subtle problem in our application. To see it,
+bring our blog application up again, and add a post. Then reload the
+page. Reload the page again.
+
+What's happening is a well-known problem: it's an instance of the
+"double-submit" problem. If a user presses reload, a request is sent
+over to our application. This wouldn't be such a bad thing, if not
+for the fact that we're handling certain requests by mutating our
+application's data structures.
+
+A common pattern that web developers use to dodge the double
+submission problem is to handle state-mutating request in a peculiar
+way: once the user sends over a request that affects change to the
+system, we then redirect them off to a different URL that's safe to
+reload. To make this happen, we will use the function @scheme[redirect/get].
+
+@defthing[redirect/get (-> request?)]
+
+This @scheme[redirect/get] function has an immediate side effect: it forces the
+user's browser to follow a redirection to a safe URL, and gives us
+back that fresh new request.
+
+For example, let's look at a toy application that lets the users add
+names to a roster:
+
+@external-file["no-use-redirect.ss"]
+
+This application suffers the same problem as our blog: if the user
+adds a name, and then presses reload, then the same name will be added
+twice.
+
+We can fix this by changing a single expression. Can you see what
+changed?
+
+@external-file["use-redirect.ss"]
+
+Double-submit, then, is painlessly easy to mitigate. Whenever we have
+handlers that mutate the state of our system, we use @scheme[redirect/get] when
+we send our response back.
+
+@bold{Exercise.}
+Revise the blog application with @scheme[redirect/get] to address the
+double-submit problem.
+
+With these minor fixes, our blog application now looks like this:
+
+@external-file["iteration-7.ss"]
+
+@section{Abstracting the Model}
+@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-8
+ web-server/scribblings/tutorial/examples/model)]
+
+If we "turn off the lights" by closing the program, then the state of
+our application disappears into the ether. How do we get our
+ephemeral state to stick around? Before we tackle that question, we
+should consider: what do we want to save? There's some state that we
+probably don't have a lingering interest in, like requests. What we
+care about saving is our model of the blog.
+
+If we look closely at our web application program, we see a seam
+between the model of our blog, and the web application that uses that
+model. Let's isolate the model: it's all the stuff near the top:
+
+@schemeblock[
+ (define-struct blog (posts) #:mutable)
+ (define-struct post (title body comments) #:mutable)
+ (define BLOG ...)
+ (define (blog-insert-post! ...) ...)
+ (define (post-insert-comment! ...) ...)
+]
+
+In realistic web applications, the model and the web application are
+separated by some wall of abstraction. The theory is that, if we do
+this separation, it should be easier to then make isolated changes
+without breaking the entire system. Let's do this: we will first rip
+the model out into a separate file. Once we've done that, then we'll
+look into making the model persist.
+
+Create a new file called @filepath{model.ss} with the following content.
+
+@external-file["model.ss"]
+
+This is essentially a cut-and-paste of the lines we identified as our
+model. There's one additional expression that looks a little odd at first:
+
+@schemeblock[
+ (provide (all-defined-out))
+]
+
+which tells PLT Scheme to allow other files to have access to
+everything that's defined in the @filepath{model.ss} file. There are
+situations where we may want to hide things for the sake of s private
+functions, or the internal representation of structures, in which case
+the pro
+
+We change our web application to use this model. Going back to our
+web application, we rip out the old model code, and replace it with an
+expression that let's use use the new model.
+
+@schemeblock[
+ (require "model.ss")
+]
+
+which hooks up our web application module to the @schememodname["model.ss"] module.
+
+@external-file["iteration-8.ss"]
+
+@section{A Model of Persistent}
+@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-9
+ web-server/scribblings/tutorial/examples/model-2)]
+
+Now that the model is separated into a separate module, we can more easily modify
+its functionality, and in particular, make it persistent.
+
+The first step is to make the model structures serializable. Earlier, we made the
+structures mutable by adding @scheme[#:mutable] to their definitions. We can make
+the structures serializable by adding @scheme[#:prefab]. This tells PLT Scheme that
+these structures can be "previously fabricated", that is, created before the program
+started running---which is exactly what we want when restoring the blog data from disk.
+Our blog structure definition now looks like:
+
+@schemeblock[
+ (define-struct blog (posts) #:mutable #:prefab)
+]
+
+Now @scheme[blog] structures can be read from the outside world with @scheme[read] and written
+with @scheme[write]. However, we need to make sure everything inside a @scheme[blog] structure is
+also marked as @scheme[#:prefab]. If we had a more complicated structure, we would need to ensure
+that everything (transitively) in the structure was @scheme[#:prefab]'d.
+
+@bold{Exercise.} Write the new structure definition for posts.
+
+At this point, we @emph{can} read and write the blog to disk. Now let's actually do it.
+
+First, we'll make a place to record in the model where the blog lives on disk. So, we need to change
+the blog structure again. Now it will be:
+
+@defstruct[blog ([home string?] [posts (listof post?)]) #:mutable]
+
+@bold{Exercise.} Write the new structure definition for blogs.
+
+Then, we'll make a function that allows our application to initialize the blog:
+
+@schemeblock[
+@code:comment{initialize-blog! : path? -> blog}
+@code:comment{Reads a blog from a path, if not present, returns default}
+(define (initialize-blog! home)
+ (define the-blog
+ (with-handlers
+ ([exn? (lambda (exn)
+ (make-blog
+ (path->string home)
+ (list (make-post "First Post"
+ "This is my first post"
+ (list "First comment!"))
+ (make-post "Second Post"
+ "This is another post"
+ (list)))))])
+ (with-input-from-file home
+ read)))
+ (set-blog-home! the-blog (path->string home))
+ the-blog)
+]
+
+@scheme[initialize-blog!] takes a path and tries to @scheme[read] from it. If the path contains
+a @scheme[blog] structure, then @scheme[read] will parse it, because @scheme[blog]s are @scheme[#:prefab].
+If there is no file at the path, or if the file has some spurious data, then @scheme[read] or
+@scheme[with-input-from-file] will throw an exception. @scheme[with-handlers] provides an
+exception handler that will return the default @scheme[blog] structure for all
+kinds of errors.
+
+After @scheme[the-blog] is bound to the newly read (or default) structure, we set the home to the
+correct path. (Notice that we need to convert the path into a string. Why didn't we just make the blog
+structure contain paths? Answer: They can't be used with @scheme[read] and @scheme[write].)
+
+Next, we will need to write a function to save the model to the disk.
+
+@schemeblock[
+@code:comment{save-blog! : blog -> void}
+@code:comment{Saves the contents of a blog to its home}
+(define (save-blog! a-blog)
+ (with-output-to-file (blog-home a-blog)
+ (lambda () (write a-blog))
+ #:exists 'replace))
+]
+
+@scheme[save-blog!] @scheme[write]s the model into its home .
+It provides @scheme[with-output-to-file] with an @scheme[#:exists] flag that tells it to replace the
+file contents if the file at @scheme[blog-home] exists.
+
+This function can now be used to save the blog structure whenever we modify it. Since we only ever modify the
+blog structure in the model, we only need to update @scheme[blog-insert-post!] and @scheme[post-insert-comment!].
+
+@bold{Exercise.} Change @scheme[blog-insert-post!] and @scheme[post-insert-comment!] to call @scheme[save-blog!].
+
+@centerline{------------}
+
+You may have had a problem when trying to update @scheme[post-insert-comment!]. It needs to call @scheme[save-blog!]
+with the blog structure. But, it wasn't passed the blog as an argument. We'll need to add that argument and change the
+application appropriately. While we're at it, let's change @scheme[blog-insert-post!] to accept the contents of the
+post structure, rather the structure itself, to better abstract the model interface:
+
+@defthing[blog-insert-post! (blog? string? string? . -> . void)]
+@defthing[post-insert-comment! (blog? post? string? . -> . void)]
+
+@bold{Exercise.} Write the new definitions of @scheme[blog-insert-post!] and @scheme[post-insert-comment!].
+(Remember to call @scheme[save-blog!].)
+
+We'll change the @scheme[provide] line in the model to:
+@schemeblock[
+(provide blog? blog-posts
+ post? post-title post-body post-comments
+ initialize-blog!
+ blog-insert-post! post-insert-comment!)
+]
+
+@centerline{------------}
+
+The last step is to change the application. We need to call @scheme[initialize-blog!] to read in the blog structure, and we
+need to pass the blog value that is returned around the application, because there is no longer a @scheme[BLOG] export.
+
+First, change @scheme[start] to call @scheme[initialize-blog!] with a path in our home directory:
+
+@schemeblock[
+ (define (start request)
+ (render-blog-page
+ (initialize-blog!
+ (build-path (find-system-path 'home-dir)
+ "the-blog-data.db"))
+ request))
+]
+
+@bold{Exercise.} Thread the @scheme[blog] structure through the application appropriately to give
+@scheme[blog-insert-post!] and @scheme[post-insert-comment!] the correct values. (You'll also need to
+change how @scheme[render-blog-page] adds new posts.)
+
+@centerline{------------}
+
+Our model is now:
+
+@external-file["model-2.ss"]
+
+And our application is:
+
+@external-file["iteration-9.ss"]
+
+@centerline{------------}
+
+This approach to persistence can work surprisingly well for simple applications. But as our application's needs
+grow, we will have to deal with concurrency issues, the lack of a simply query language over our data model, etc.
+So, in a future tutorial, we'll talk about how to use an SQL database to store our blog model.
+
+@section{Leaving DrScheme}
+
+So far, to run our application, we've been pressing @onscreen{Run} in DrScheme. If we were to actually deploy
+an application, we'd need to do this differently.
+
+@(require (for-label web-server/servlet-env)
+ (for-label web-server/managers/lru))
+
+The simplest way to do this is to use @schememodname[web-server/servlet-env].
+
+First, change the first lines in your application from
+@schememod[
+web-server/insta
+]
+
+to
+@schememod[
+scheme
+
+(require web-server/servlet)
+(provide/contract (start (request? . -> . response?)))
+]
+
+Second, add the following at the bottom of your application:
+
+@schemeblock[
+(require web-server/servlet-env
+ web-server/managers/lru)
+(serve/servlet start
+ #:launch-browser? #f
+ #:quit? #f
+
+ #:listen-ip #f
+ #:port 8000
+
+ #:manager
+ (make-threshold-LRU-manager
+ (lambda (request)
+ `(html
+ (head (title "Page Has Expired."))
+ (body (p "Sorry, this page has expired. "
+ "Please go back."))))
+ (* 64 1024 1024))
+
+ #:extra-files-path
+ (build-path _path "htdocs")
+ #:servlet-path
+ [servlet-path "servlets/APPLICATION.ss"])
+]
+
+You can change the value of the @scheme[#:port] parameter to use a different port.
+
+@scheme[#:listen-ip] is set to @scheme[#f] so that the server will listen on @emph{all} available IPs.
+
+The number given to the @scheme[make-threshold-LRU-manager] specifies a memory limit for the Web server's
+session data. (In the code above, it is 64 MBs.)
+
+You should change @scheme[_path] to be the path to the parent of your @scheme[htdocs] directory.
+
+You should change @scheme["APPLICATION.ss"] to be the name of your application.
+
+Third, to run your server, you can either press @onscreen{Run} in DrScheme, or type
+
+@commandline{mzscheme -t }
+
+(With your own file name, of course.) Both of these will start a Web server @emph{just} for your application.
+
+@centerline{------------}
+
+There are more advanced ways of starting the Web Server, but you'll have to
+refer to the PLT Web Server Reference Manual for details.
+
+@section{Moving Forward}
+
+As you move forward on your own applications, you may find many useful packages on PLaneT. There are interfaces to
+databases. Many tools for generating HTML, XML, and Javascript output. Etc. There is also an active community of
+users on the @scheme[plt-scheme] mailing list. We welcome new users!
\ No newline at end of file