diff --git a/contrib/cont-responder/cont-numbers-game.factor b/contrib/cont-responder/cont-numbers-game.factor new file mode 100644 index 0000000000..1e79e079e8 --- /dev/null +++ b/contrib/cont-responder/cont-numbers-game.factor @@ -0,0 +1,103 @@ +! cont-number-guess +! +! Copyright (C) 2004 Chris Double. +! +! Redistribution and use in source and binary forms, with or without +! modification, are permitted provided that the following conditions are met: +! +! 1. Redistributions of source code must retain the above copyright notice, +! this list of conditions and the following disclaimer. +! +! 2. Redistributions in binary form must reproduce the above copyright notice, +! this list of conditions and the following disclaimer in the documentation +! and/or other materials provided with the distribution. +! +! THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, +! INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +! FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +! DEVELOPERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +! SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +! PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +! OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +! WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +! OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +! ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +! +! This example modifies the console based 'numbers-game' example +! in a very minimal way to demonstrate conversion of a console +! program to a web based application. +! +! All that was required was changing the input and output functions +! to use HTML. The remaining code was untouched. +! +! The result is not that pretty but it shows the basic idea. +IN: numbers-game +USE: combinators +USE: kernel +USE: math +USE: random +USE: parser +USE: html +USE: cont-responder +USE: cont-utils +USE: stack +USE: stdio +USE: namespaces + +: web-print ( str -- ) + #! Display the string in a web page. + [ + swap dup + + write + +

write

+

"Press to continue" write

+ + + ] show 2drop ; + +: read-number ( -- ) + [ + + "Enter a number" write + +
+

+ "Enter a number:" write + + +

+
+ + + ] show [ "num" get ] bind parse-number ; + +: guess-banner + "I'm thinking of a number between 0 and 100." web-print ; +: guess-prompt "Enter your guess: " web-print ; +: too-high "Too high" web-print ; +: too-low "Too low" web-print ; +: correct "Correct - you win!" web-print ; +: inexact-guess ( actual guess -- ) + < [ too-high ] [ too-low ] ifte ; + +: judge-guess ( actual guess -- ? ) + 2dup = [ + 2drop correct f + ] [ + inexact-guess t + ] ifte ; + +: number-to-guess ( -- n ) 0 100 random-int ; + +: numbers-game-loop ( actual -- ) + dup guess-prompt read-number judge-guess [ + numbers-game-loop + ] [ + drop + ] ifte ; + +: numbers-game number-to-guess numbers-game-loop ; + +"numbers-game" [ numbers-game ] install-cont-responder \ No newline at end of file diff --git a/contrib/cont-responder/cont-responder.factor b/contrib/cont-responder/cont-responder.factor index 19f1b134ed..540cae6a1f 100644 --- a/contrib/cont-responder/cont-responder.factor +++ b/contrib/cont-responder/cont-responder.factor @@ -44,6 +44,9 @@ USE: url-encoding USE: unparser USE: hashtables +USE: prettyprint +USE: inspector + : expiry-timeout ( -- timeout-seconds ) #! Number of seconds to timeout continuations in #! continuation table. This value will need to be @@ -194,9 +197,7 @@ DEFER: show : with-string-stream ( quot -- string ) #! Call the quotation with standard output bound to a string output #! stream. Return the string on exit. - [ - "stdio" 1024 put call "stdio" get stream>str - ] bind ; + 1024 dup >r swap with-stream r> stream>str ; : redirect-to-here ( -- ) #! Force a redirect to the client browser so that the browser @@ -234,8 +235,7 @@ DEFER: show call-exit-continuation ] callcc1 nip ; -USE: prettyprint -USE: inspector + : cont-get-responder ( id-or-f -- ) #! httpd responder that retrieves a continuation and calls it. @@ -254,17 +254,12 @@ USE: inspector ] ifte [ write flush ] when* drop ; -: post-request>namespace ( post-request -- namespace ) - #! Return a namespace containing the name/value's from the - #! post data. - alist>hash ; - : cont-post-responder ( id -- ) #! httpd responder that retrieves a continuation for the given - #! id and calls it with the POST data as an alist on the top + #! id and calls it with the POST data as a hashtable on the top #! of the stack. [ - "response" get post-request>namespace swap resume-continuation + "response" get alist>hash swap resume-continuation ] with-exit-continuation print drop ; diff --git a/contrib/cont-responder/cont-testing.factor b/contrib/cont-responder/cont-testing.factor new file mode 100644 index 0000000000..a9b4f16b40 --- /dev/null +++ b/contrib/cont-responder/cont-testing.factor @@ -0,0 +1,114 @@ +! cont-testing +! +! Copyright (C) 2004 Chris Double. +! +! Redistribution and use in source and binary forms, with or without +! modification, are permitted provided that the following conditions are met: +! +! 1. Redistributions of source code must retain the above copyright notice, +! this list of conditions and the following disclaimer. +! +! 2. Redistributions in binary form must reproduce the above copyright notice, +! this list of conditions and the following disclaimer in the documentation +! and/or other materials provided with the distribution. +! +! THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, +! INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +! FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +! DEVELOPERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +! SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +! PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +! OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +! WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +! OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +! ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +! +! Words for testing continuation based responders at the console +! prompt. +! +! To start a 'test' session use '' to push the +! continuation responder state on the stack. +! +! Then use 'test-cont-function' to call a continuation responder word. +! All output will go to the console. From this output you will see +! links that you can 'visit' by doing a simulated click. Use the +! 'test-cont-click' function by passing the state, the 'id' of the click +! continuation, and 'f' or a hashtable containing the post data. The output +! from this will be displayed. +! +! eg. +! [ test-cont-responder ] test-cont-function +! => HTTP/1.1 302 Document Moved +! Location: 8506502852110820 +! Content-Length: 0 +! Content-Type: text/plain +! +! "8506502852110820" f test-cont-click +! => HTTP/1.0 200 Document follows +! Content-Type: text/html +! +! Page one +!

Page one

Next +! +! +! "5431597582800278" f test-cont-click +! => HTTP/1.1 302 Document Moved +! Location: 7944183606904129 +! Content-Length: 0 +! Content-Type: text/plain +! +! "7944183606904129" f test-cont-click +! => HTTP/1.0 200 Document follows +! Content-Type: text/html +! +! Enter your name +!

Enter your name

+!
+! Name: +! +!
+! +! "8503790719833723" [ [ "name" | "Chris" ] ] alist>hash test-cont-click +! => HTTP/1.1 302 Document Moved +! Location: 8879727708050260 +! Content-Length: 0 +! Content-Type: text/plain +! +! "8879727708050260" f test-cont-click +! => HTTP/1.0 200 Document follows +! Content-Type: text/html +! +! Hello Chris +!

Hello Chris

+! Next +! +! +! etc. +IN: cont-responder +USE: namespaces +USE: stack +USE: combinators +USE: stdio + +: ( -- ) + #! Create a namespace holding data required + #! for testing continuation based responder functions + #! at the interpreter console. + [ + reset-continuation-table + init-session-namespace + ] extend ; + +: test-cont-function ( quot -- ) + #! Call a continuation responder function with required + #! plumbing set up so output is displayed to the console. + swap dup >r [ + [ call ] with-exit-continuation + ] bind write drop r> ; + +: test-cont-click ( id data -- ) + #! Test function to 'click' a continuation with the given + #! 'id' and post data. Display the results on the console. + rot dup >r [ + [ swap resume-continuation ] with-exit-continuation + ] bind write 2drop r> ; diff --git a/contrib/cont-responder/eval-responder.factor b/contrib/cont-responder/eval-responder.factor index 8b25706ef0..4cc4af7c64 100644 --- a/contrib/cont-responder/eval-responder.factor +++ b/contrib/cont-responder/eval-responder.factor @@ -39,7 +39,10 @@ USE: logic USE: combinators USE: live-updater USE: prettyprint +USE: unparser USE: words +USE: vectors +USE: logging : ( stack msg history -- ) #! Create an 'evaluator' object that holds @@ -71,13 +74,12 @@ USE: words : escape-quotes ( string -- string ) #! Replace occurrences of single quotes with #! backslash quote. - [ dup [ [ "'" | "\\'" ] [ "\"" | "\\\"" ] ] assoc dup rot ? ] str-map ; + [ dup [ [ CHAR: ' | "\\'" ] [ CHAR: " | "\\\"" ] ] assoc dup rot ? ] str-map ; : make-eval-javascript ( string -- string ) #! Give a string return some javascript that when #! executed will set the eval textarea to that string. [ "document.forms.main.eval.value=\"" , escape-quotes , "\"" , ] make-string ; - : write-eval-link ( string -- ) #! Given text to evaluate, create an A HREF link which when #! clicked sets the eval textarea to that value. @@ -87,7 +89,7 @@ USE: words #! Write out html to display the stack. - [ ] each + [ ] each
"Callstack" write
write-eval-link
[ unparse write ] with-string-stream write-eval-link
; : display-clear-history-link ( -- ) @@ -112,7 +114,7 @@ USE: words "responder" "inspect" put - + @@ -169,10 +171,18 @@ USE: words "eval" get ] bind ; +: infra ( list quot -- list ) + #! Call the quotation using 'list' as the datastack + #! return the result datastack as a list. + datastack >r + swap list>vector tuck vector-push + set-datastack call datastack vector>list + r> >pop> >pop> tuck vector-push set-datastack ; + : do-eval ( list string -- list ) #! Evaluate the expression in 'string' using 'list' as #! the datastack. Return the resulting stack as a list. - parse unit append restack call unstack ; + parse infra ; : do-eval-to-string ( list string -- list string ) #! Evaluate expression using 'list' as the current callstack. @@ -202,8 +212,9 @@ USE: words [ run-eval-requester ] [ - show-message-page + dup [ show-message-page ] [ drop ] ifte ] catch ] forever ; "eval" [ [ ] "None" [ ] eval-responder ] install-cont-responder + diff --git a/contrib/cont-responder/load.factor b/contrib/cont-responder/load.factor index a8a6e302e2..fcb1503e8a 100644 --- a/contrib/cont-responder/load.factor +++ b/contrib/cont-responder/load.factor @@ -38,13 +38,15 @@ USE: parser "cont-responder.factor" run-file "cont-utils.factor" run-file ; : l2 - "cont-examples.factor" run-file ; + "cont-examples.factor" run-file + "cont-numbers-game.factor" run-file ; : l3 "todo.factor" run-file ; : l4 "todo-example.factor" run-file ; : l5 "live-updater.factor" run-file ; -! : l6 "eval-responder.factor" run-file ; +: l6 "eval-responder.factor" run-file ; : l7 "live-updater-responder.factor" run-file ; : l8 "browser.factor" run-file ; +: l9 "cont-testing.factor" run-file ; : la ; : la [ 8888 httpd ] [ dup . flush [ la ] when* ] catch ; : lb [ la "httpd thread exited.\n" write flush ] in-thread ; diff --git a/contrib/cont-responder/tutorial.txt b/contrib/cont-responder/tutorial.txt new file mode 100644 index 0000000000..e38d67f061 --- /dev/null +++ b/contrib/cont-responder/tutorial.txt @@ -0,0 +1,407 @@ +Overview +======== + +The 'cont-responder' library is a continuation based web server +for writing web applications in Factor. Each 'web application' is a +standard Factor httpd responder. + +This document outlines how to write simple web applications using +'cont-responder' by showing examples. It does not attempt to go into +the technical details of continuation based web applications or how it +is implemented in Factor. Instead it uses a series of examples that +can be immediately tried at the Factor prompt to get a feel for how +things work. + +Getting Started +=============== +To get started you will first need to load the 'cont-responder' +code. You will need the following as a minimum: + + "cont-responder.factor" run-file + "cont-utils.factor" run-file + USE: cont-responder + USE: cont-utils + +The responders that you will be writing will require an instance of +the httpd server to be running. It will be run in a background thread +to enable the interactive development of the applications. The +following is a simple function to start the server on port 8888 and +restart it if an error occurs: + + USE: httpd + USE: threads + : start-httpd [ 8888 httpd ] [ dup . flush [ start-httpd ] when* ] catch ; + [ start-httpd ] in-thread + +Responders +========== +A 'responder' is a word that is registered with the httpd server that +gets run when the client accesses a particular URL. When run that word +has 'standard output' bound in such a way that all output goes to the +clients web browser. + +In the 'cont-responder' system the word used to set output to go to the web +browser and display a page is 'show'. Think of it as 'show a page to +the client'. 'show' takes a single item on the stack and that is a +'page generation' quotation. + +A 'page generation' quotation is a quotation with stack effect ( +string -- ). For now we'll ignore the string it receives on the +stack. Its purpose will be explained later. + +Hello World 1 +============= +A simple 'hello world' responder would be: + + : hello-world1 ( -- ) + [ + drop + "Hello World" write + "Hello World!" write + ] show drop ; + +When installed this will show a single page which is simple HTML to +display 'Hello World!'. The 'show' word returns a namespace, the +purpose of which will also be explained later. For now we ignore it +and drop it. Notice we also drop the 'URL' that the quotation passed +to 'show' receives on the stack. + +The responder is installed using: + + "helloworld1" [ hello-world1 ] install-cont-responder + +The 'install-cont-responder' word has stack effect ( name quot -- +). It installs a responder with the given name. When the URL for that +responder is accessed the 'quot' quotation is run. In this case it is +'hello-world1' which displays the single HTML page described +previously. + +Accessing the above responder from a web browser is via an URL like: + + http://localhost:8888/responder/helloworld1 + +This should display an HTML page showing 'Hello World!". + +HTML Generation +=============== +Generating HTML by writing strings containing HTML can be a bit of a +chore. Especially when the content is dynamic requiring concatenation +of many pieces of data. + +The 'cont-responder' system uses 'html', a library that allows writing +HTML looking output directly in factor. This system developed for +'cont-responder' has recently been made part of the standard 'html' +library of Factor. + +'html' basically allows you to write HTML-like output in a factor word +and it will be automatically output. It can be tested at the console +very easily: + + USE: html +

"This is a paragraph" write

+ =>

This is a paragraph

+ +You can write open and close tags like orginary HTML and anything sent +to standard output in between the tags will be enclosed in the +specified tags. Attributes can also be used: + +

"More text" write

+ =>

More text

+ +The attribute must be seperated from the value of that attribute via +whitespace. If you are using attributes the tag must be closed with a +'[tagname]>' where the [tagname] is the name of the tag used. See the +'

' example above. + +You can use any factor code at any point: + + "text-align: " "red" +

+ "Using style " write swap write write +

+ =>

Using style text-align: red

+ +Tags that are not normally closed are written using an XML style +(ie. with a trailing slash): + + "One" write
"Two" write
+ => One
Two
+ +Hello World 2 +============= + +Using the HTML generation library makes writing responders more +readable. Here is the hello world example perviously using this +system: + + : hello-world2 ( -- ) + [ + drop + + "Hello World" write + "Hello World!" write + + ] show drop ; + +Install it using: + + "helloworld2" [ hello-world2 ] install-cont-responder + +Dynamic Data +============ + +Adding dynamic data to the page is relatively easy. This example pulls +a value from the 'room' word which displays the amount of available +and free memory in the system. + + : memory-stats1 ( -- ) + [ + drop + + "Memory Statistics" write + +
"Source" write
[ see ] with-simple-html-output
[ [ parse ] [ [ "No such word" write ] [ car see ] ifte ] catch ] with-simple-html-output
"Apropos" write "Usages" write
[ apropos. ] with-simple-html-output [ usages. ] with-simple-html-output
+ + + + + + + + +
"Total Memory" write room unparse write
"Free Memory" write unparse write
+ + + ] show drop ; + + "memorystats1" [ memory-stats1 ] install-cont-responder + +Accessing this page will show a table with the current memory +statistics. Hitting refresh will update the page with the latest +information. + +The HTML output can be refactored into different words. For example: + + : memory-stats-table ( free total -- ) + #! Output a table containing the given statistcs. + + + + + + + + + +
"Total Memory" write unparse write
"Free Memory" write unparse write
; + + : memory-stats2 ( -- ) + [ + drop + + "Memory Statistics 2" write + room memory-stats-table + + ] show drop ; + + "memorystats2" [ memory-stats2 ] install-cont-responder + +Some simple flow +================ +The big advantage with continuation based web servers is being able to +write a web application in a standard procedural flow and have it +correctly served up in the HTTP request/response model. + +This example demonstates a flow of three pages. Clicking an URL on the +first page displays the second. Clicking an URL on the second displays +the third. + +When a 'show' call is executed the page generated by the quotation is +sent to the client. The computation of the responder is then +'suspended'. When the client accesses a special URL computiation is +resumed at the point of the end of the 'show' call. In this way +procedural flow is maintained. + +This brings us to the 'URL' stack item that is available to the 'page +generation' quotation passed to 'show'. This URL is a string that +contains a URL that can be embedded in the page. When the user access +that URL computation is resumed from the point of the end of the +'show' call as described above: + + : flow-example1 ( -- ) + [ + + "Flow Example 1" write + +

"Page 1" write

+

"Press to continue" write

+ + + ] show drop + [ + + "Flow Example 1" write + +

"Page 2" write

+

"Press to continue" write

+ + + ] show drop + [ + drop + + "Flow Example 1" write + +

"Page 3" write

+ + + ] show drop ; + + "flowexample1" [ flow-example1 ] install-cont-responder + +The 'flow-example1' word contains three 'show' calls in a row. The +first two display simple pages with an anchor link to the URL received +on the stack. This URL when accessed resumes the computation. The +final page just drops the URL. + +When you display this example in the browser you'll be able to click +the URL to navigate. You can use the back button to retry the URL's, +you can clone the browser window and navigate them independantly, etc. + +The similarity of the functions above could so with some +refactoring. The pages are almost exactly the same so we seperate this +into a seperate word: + + : show-flow-page ( n bool -- ) + #! Show a page in the flow, using 'n' as the page number + #! to display. If 'bool' is true display a link to the + #! next page. + [ ( n bool url -- ) + + "Flow Example 1" write + +

"Page " write rot unparse write

+ swap [ +

"Press to continue" write

+ ] [ + drop + ] ifte + + + ] show 3drop ; + + : flow-example2 ( n -- ) + #! Display the given number of pages in a row. + dup pred [ succ t show-flow-page ] times* + f show-flow-page ; + + "flowexample2" [ 5 flow-example2 ] install-cont-responder + +In this example the 'show-flow-age' pulls the page number off the +stack. It also gets whether or not to display the link to the next +page. + +Notice that after the show that a '3drop' is done whereas +previously we've only done a single 'drop'. This is due to a side +effect or 'show' using continuations. + +After the 'show' call returns there will be one item on the stack +(which we've been dropping and will explain later what it is). The +stack will also be set as it was before the show call. So in this case +the 'n' and 'bool' remain on the stack even though they were removed +during the page generation quotation. This is because we resumed the +continuation which, when captured, had those items on the stack. The +general rule of thumb is you will need to account for items on the +stack before the show call. + +This example also demonstrates using the 'times*' combinator to +sequence the page shows. Any Factor code can be called and the +continuation based system will sequentially display each page. The +back button, browser window cloning, etc will all continue to work. + +You'll notice the URL's in the browser have a number at the end of +them. This is the 'continuation identifier' which is like a session id +except that it identifies not just the data you have stored but your +location within the responder as well. + +Forms and POST data +=================== +The web pages we've generated so far don't accept input from the +user. I've mentioned previously that 'show' returns a value on the +stack and we've been dropping it in our examples. + +The value returned is a namespace containing the field names and +values of any POST data in the request. If no POST data exists then it +is the boolean value 'f'. + +To process input from the user just put a form in the HTML with a +method of 'POST' and an action set to the URL passed in to the page +generation quotation. The show call will then return a namespace +containing this data. Here is a simple example: + + : accept-users-name ( -- name ) + #! Display an HTML requesting the users name. Push + #! the name the user input on the stack.. + [ + + "Please enter your name" write + +
+

+ "Please enter your name:" write + + +

+
+ + + ] show [ + "username" get + ] bind ; + + : post-example1 ( -- ) + [ + drop + + "Hello!" write + +

accept-users-name write ", Good to see you!" write

+ + + ] show drop ; + + "post-example1" [ post-example1 ] install-cont-responder + +The 'accept-users-name' word displays an HTML form allowing input of +the name. When that form is submitted the namespace containing the +data is returned by 'show'. We bind to it and retrieve the 'username' +field. The name used here should be the same name used when creating +the field in the HTML. + +'post-example1' then does something a bit tricky. Instead of first +calling 'accept-users-name' to push the name on the stack and then +displaying the resulting page we call 'accept-users-name' from within +the page itself when we actually need it. The magic of the +continuation system causes the 'accept-users-name' to be called when +needed displaying that page first. It is certainly possible to do it +the other way though: + + : post-example2 ( -- ) + accept-users-name + [ ( name url -- ) + drop + + "Hello!" write + +

write ", Good to see you!" write

+ + + ] show 2drop ; + + "post-example2" [ post-example2 ] install-cont-responder + +Either way works. Notice that in the 'post-example2' we had to do a +'2drop' instead of a 'drop' at the end of the show to remove the +additional 'name' that is on the stack. This wasn't needed in +'post-example1' because the 'name' was not on the stack at the time of +the 'show' call. \ No newline at end of file diff --git a/contrib/parser-combinators/parser-combinators.factor b/contrib/parser-combinators/parser-combinators.factor index edb9c8b015..a1850fe01c 100644 --- a/contrib/parser-combinators/parser-combinators.factor +++ b/contrib/parser-combinators/parser-combinators.factor @@ -45,7 +45,7 @@ USE: parser #! For a string this is everything but the first character. #! For a list this is the cdr. [ - [ string? ] [ 1 str-tail ] + [ string? ] [ 1 swap str-tail ] [ list? ] [ cdr ] ] cond ; @@ -77,7 +77,7 @@ USE: parser dup str-length pick < [ 2drop "" ] [ - swap str-head + str-head ] ifte ; : (list-take) ( n list accum -- list ) @@ -107,7 +107,7 @@ USE: parser dup str-length pick < [ 2drop "" ] [ - swap str-tail + str-tail ] ifte ; : list-drop ( n list -- list )