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 0000000000..e16536fda7 Binary files /dev/null and b/collects/web-server/scribblings/tutorial/images/Flow1.dia differ diff --git a/collects/web-server/scribblings/tutorial/images/Flow1.png b/collects/web-server/scribblings/tutorial/images/Flow1.png new file mode 100644 index 0000000000..c5c94e7436 Binary files /dev/null and b/collects/web-server/scribblings/tutorial/images/Flow1.png differ 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 0000000000..eed3017cae Binary files /dev/null and b/collects/web-server/scribblings/tutorial/images/Flow2.dia differ diff --git a/collects/web-server/scribblings/tutorial/images/Flow2.png b/collects/web-server/scribblings/tutorial/images/Flow2.png new file mode 100644 index 0000000000..7ce92ebf56 Binary files /dev/null and b/collects/web-server/scribblings/tutorial/images/Flow2.png differ 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 0000000000..eb1275a316 Binary files /dev/null and b/collects/web-server/scribblings/tutorial/images/Flow3.dia differ diff --git a/collects/web-server/scribblings/tutorial/images/Flow3.png b/collects/web-server/scribblings/tutorial/images/Flow3.png new file mode 100644 index 0000000000..ea285136ca Binary files /dev/null and b/collects/web-server/scribblings/tutorial/images/Flow3.png differ diff --git a/collects/web-server/scribblings/tutorial/sql.scrbl b/collects/web-server/scribblings/tutorial/sql.scrbl new file mode 100644 index 0000000000..791b2464d8 --- /dev/null +++ b/collects/web-server/scribblings/tutorial/sql.scrbl @@ -0,0 +1,170 @@ + +@section{Using an SQL database} +@declare-exporting[#:use-sources ("iteration-10.ss" + "model-3.ss")] +@(require (prefix-in sqlite: (for-label (planet jaymccarthy/sqlite:3/sqlite)))) + +Our next task is to employ an SQL database for the blog model. We'll be using SQLite with the @schememodname[(planet jaymccarthy/sqlite:3/sqlite)] PLaneT package. We add the following to the top of our model: + +@schemeblock[ +(require (prefix-in sqlite: (planet jaymccarthy/sqlite:3/sqlite))) +] + +We now have the following bindings: + +@defthing[sqlite:open (path? . -> . 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