REBOL/Zine
Volume 1 Issue 2
Date: 29-Jun-2001
Issues: prev | next
=== Lit-Word!
I think lit-word! is a good title for this little opening chunk of
text where I have the distinct honor of writing what's on my mind
shortly before completing compiling and firing off the distribution
of REBOL/Zine.
The momentum is definitely here, folks. This time it took little to
no arm twisting to get authors to send in some very nice articles.
But, since I'm really into arm twisting, please keep sending them in!
(rebolzine@yahoo.com)
All other considerations aside, the knowledge of REBOL is truly
something that can help you in life. Even if REBOL is not the
language you use at work, REBOL can help you work. REBOL's the wicked
little tool in your tool box that Joe Bob (the programmer next to you
who thinks Java is the "End All") is sorely missing in his generic
cookie cutter tool box.
Joe Bob sits down and furiously works out on paper some complicated
design for a GUI that he hopes to implement over the next few weeks
(years). You sit down and whip something together in View and have it
up on the screen in minutes. Joe Bob meekly looks on from under his
big UML mess of ink and inheritance insanity while you gently tweak
your working prototype into clarity. Joe Bob feels as obsolete as a
pile of punch cards. That's okay, just show him how easy it is to
do. Then he'll just feel unaware.
Just a little kidding around.. (-:
The advancement of REBOL knowledge is the charter of the REBOL/Zine.
REBOL can change your life, and it could change Joe Bob's. Teach
REBOL and learn REBOL and spread the revolution!
[Once again, not as much proof reading happened as I'd like (though
more than the first time!), so please forgive any written goofs you
may find. (-:]
=== REBOL puzzLES and idioms
by Jeff Kreis
--- Brain Binder
Try to figure out what will the result of the following be, and then
check your answers in a REBOL session:
use [x y z v][
y: in set 'x v: make object! [y: 2] 'y
x: make object! compose [
y: v/y (set y 4)
z: reduce bind compose [
(set in x 'y 7 'y) ; x/z/1
] 'v
]
probe reduce [get x/z/1 x/y get y]
]
If you can guess the answer before running the code, you've probably
got a good handle on BIND, objects, and evaluation. If you didn't
guess right before running it, then you're probably sane. :-)
--- If IF Were Different
One aspect of REBOL that makes it so flexible is that REBOL is "First
Class". Of course, we know that REBOL's top rate, but First Class
means something quite specific in terms of computer science. One way
that I've heard First Class defined is that "nothing in the language
is privileged... you can redefine IF if you want." This definition
sets First Class languages aside from languages which have reserved
words, words like IF that you are not allowed to define as a new
function. Well, usually this explanation of First Class is followed
by "I don't know why you'd want to redefine IF, but if you wanted to,
you could." And sure enough you can redefine IF in REBOL:
if-orig: :if
if: func [cond then-block /else else-block][
print "!NEWIF!"
either else [
if-orig/else cond then-block else-block
][
if-orig cond then-block
]
]
if 1 = 1 [print "one equals one"]
Prints:
!NEWIF!
one equals one
Everything in REBOL will continue to operate oblivious of the fact
that you've changed IF. You can see how often IF is called when doing
something like: read http://www.rebol.com
So, why would you want to redefine IF? What problem can you imagine
that would be solved by redefining IF? This is an open ended
question. If you have an answer (serious or silly), send it in and
we'll run the answers in a subsequent episode.
-Jeff Kreis
jeff at rebol dot com
=== Dynamic face positioning
by Christophe Coussement
It's sometimes tricky to find the right place for all the faces on the
background - you known ... set offset, run, modify offset, run, modify
again ...
Graham Chiu [gchiu at compkarori dot co dot nz] told me about a GUI
design tool developed by Carl, but he couldn't find it back. It seemed
to be included as an example when View was first released but he
hasn't seen it since. [Editors note: 'Haiku and 'Layout]
To solve the problem I thought it should be great doing that on
"intuitive" way ... like drag&drop .
So I hacked the code by creating a block containing the ENGAGE
function, and I call it within the FEEL facet from the face
I will place:
move: [
engage: func [face action event][
if action = 'down [start: event/offset]
if find [over away] action [
face/offset: face/offset + event/offset - start
print face/offset
show face
]
]
]
lay: layout/size [
box 50x50 "Hello" red feel move ;<<<<<
slider 10x100 feel move ;<<<<<
] 200x200
view lay
Because of the PRINT FACE/offset, I can read the current position of
the face I'm moving. When I'm happy with it, I just note that and set
the object static by an AT or OFFSET or whatever.
When all is OK, I just remove the FEEL MOVE code.
It's a quick hack, but it helps a lot!
Now, if you plan to use this often, add the MOVE block to the %user.r
file:
feel-ctx: context [
set 'move [
engage: func [face action event][
if action = 'down [start: event/offset]
if find [over away] action [
face/offset: face/offset + event/offset - start
print face/offset
show face
]
]
]
]
Note: you can find the description and use of the FEEL facet into the
how-to at:
=url http://www.rebol.com/how-to/feel.html http://www.rebol.com/how-to/feel.html
coussement dot c at js dot mil dot be
=== Creating a Log File
by Colin Sheperton
If you're intending to write an application more than halfway complicated,
you may want a log file. You can write status or debugging items to it, and
then analyse it when and if problems occur.
The log file code here is out of my "standard utilities" tool box. It was one
of the first things I wrote in Rebol, so it doesn't use any clever code, but
it is well-commented and could help someone trying to get to grips with the
language.
Here's how to use it.
First cut'n'paste the code into a file (eg STDUTILS.R, though any name will
do), and then load the code by:
do %STDUTILS.R
Second, set the variable stdutils/LogFileName to a string that is a valid
file name, eg:
;; to log in the current directory:
stdutils/LogFileName: "LogFile.txt"
;;to log elsewhere:
stdutils/LogFileName: "/c/temp/log.txt"
You can then call stdutils/WriteLogEntry to add entries to the log:
stdutils/LogFileName: "logfile.txt"
MyNumber: 33
Mytuple: 5.6.5
MyString: "hello"
MyLongString: {aaa ^/bbbb^/cccc^/ddd}
MyBlock: [1 2 3 4 5]
MyObject: make object! [a: 111 b: 222 c: 333]
stdutils/WriteLogEntry MyNumber
stdutils/WriteLogEntry MyTuple
stdutils/WriteLogEntry MyString
stdutils/WriteLogEntry MyLongString
stdutils/WriteLogEntry MyBlock
stdutils/WriteLogEntry MyObject
stdutils/WriteLogEntry join "time is now " to-string now/time
stdutils/WriteLogEntry Join "memory usage is " system/stats
stdutils/WriteLogEntry "Stop logging now"
stdutils/LogFileName: false
stdutils/WriteLogEntry "This isn't logged"
The log will read something like:
= = = = = = = = = = = = =
<27-Jun-2001 10:17:52 integer.> Log Started
<27-Jun-2001 10:17:52 integer.> 33
<27-Jun-2001 10:17:52 tuple...> 5.6.5
<27-Jun-2001 10:17:52 string..> hello
<27-Jun-2001 10:17:52 string..> aaa
<27-Jun-2001 10:17:52 string..> ...bbbb
<27-Jun-2001 10:17:52 string..> ...cccc
<27-Jun-2001 10:17:52 string..> ...ddd
<27-Jun-2001 10:17:52 block...> [1 2 3 4 5]
<27-Jun-2001 10:17:53 object..> make object! [
<27-Jun-2001 10:17:53 object..> ...a: 111
<27-Jun-2001 10:17:53 object..> ...b: 222
<27-Jun-2001 10:17:53 object..> ...c: 333
<27-Jun-2001 10:17:53 object..> ...]
<27-Jun-2001 10:17:53 string..> time is now 10:17:52
<27-Jun-2001 10:17:53 string..> memory usage is 3802696
<27-Jun-2001 10:17:53 string..> Stop logging now
= = = = = = = = = = = = =
MyLongString and MyObject have been split over several entries for ease of
reading.
You can switch to multiple logs simply by changing the value of
stdutils/LogFileName, for example, this function always writes to a specific
log file:
WriteStatusLog: func [ItemtoLog [Any-type!]
/local Savedname
]
[
SavedName: stdUtils/LogFileName
stdUtils/LogFileName: "/c/temp/StatusLog.txt"
stdUtils/WriteLogEntry ItemToLog
stdutils/logfilename: SavedName
Return true
]
--- The code
(I hope the comments survive line-wrapping. If it's a complete mess,
email me and I'll send you an attachment)
Rebol [Title: "Standard Utilities"]
stdutils: Make object!
[
LogFileName: false ; Default is no logging
WriteLogEntry: func [{writes a log record. Use stdutils/LogFileName to
control whether to write, and to where.}
p-Item [any-type!] "Item to be logged"
/local FileName Log-Prefix
Log-Lines Log-Line
Cont-Ind Item-copy]
[
if LogFileName = false ; Nothing to do if false
[Return false]
File-Name: to-file trim LogFileName ; Remove file name whitespace
either (type? p-Item) = String! ; Make sure log item
[Item-Copy: p-item] ; is in a
[Item-Copy: mold p-item] ; printable form
Log-Prefix: join "<" [to-string now/date ; Create
" " ; log
to-string now/time ; line
" " ; prefix
type? p-Item ;
]
while [(length? Log-Prefix) < 30] ; ensure prefix is
[append Log-Prefix "."] ; fixed length for
append Log-Prefix "> " ; easy log reading
if not exists? File-Name ; Create & write first line
[write/lines ; if log file does not exist.
File-Name ;
join Log-Prefix "Log Started"
] ;if
Log-Lines: parse/all Item-copy "^/" ; Split the data
; to be logged into
; separate lines
Cont-Ind: ""
foreach Log-Line Log-Lines
[
if (length? trim Log-Line) > 0 ; Write a line
[write/lines/append ; if it has
File-Name ; any data
join Log-Prefix
[Cont-Ind Log-Line]
Cont-Ind: "..." ; Set "..." for 2nd+ lines
] ;if
] ;for
return (Cont-Ind = "...") ; Return with True or False:
; (Cont-Ind is "..." if we wrote at
; least one non-zero length line).
] ;func
] ; object
Finally, here's a couple of notes on the coding technique in case you are new
to Rebol Objects. The function WriteLogEntry is within an Object called
StdUtils:
StdUtils: make object
[
...
WriteLogEntry: func [...]
...
]
I could have written it as a standalone function, not in an Object. With a
small project, or a function you expect to use in only one program, that's
fine. But if the function is likely to have a wider life, then we need some
way of insuring its name doesn't clash with someone else's function of the
same name. Putting it in an object creates a two-part name, object/function,
thus reducing the risk of a clash. It's a bit like having a surname and a
first name.
The technique isn't bulletproof: someone else may have thought if calling
their Standard Utilities Object "stdutils", and they may have a "WriteLogEntry
function" too. Perhaps I should have called my object "CS-Utils", reducing
the risk of a clash even further.
The variable LogFileName is defined within the Object, giving it a two-part
name (stdutils/LogFileName) and so reducing the chance of a name clash. But
it is not defined in the Function WriteLogEntry. If it was, there would be no
easy way of getting hold of it to set a value. So the object has the
structure:
stdutils: make object
[ ...define variables global within object
...define functions
]
See the Core Users' Guide chapter on Objects for more details on this.
The object itself is in a source file called STDUTIL.R. It makes sense for it
to be the only object in the source file and for the name to be the same. But
neither of these are essential. I could have split my standard utilities into
several objects, and put them all in one file, eg:
ALLMYUTILITIES.R could contain:
stdutils: make object
[ ...define variables global within stdutils
...define functions–including WriteLogEntry
]
mathUtils: make object
[ ...define variables global within mathutils
...define functions
]
I'd now "activate" all my utilities by writing:
DO %ALLMYUTILITIES.R
and thereafter I can use stdutils/WriteLogEntry as before.
Colin Sheperton,
sanghabum at aol dot com
=== Dining with Dynamic Interpreters
by Jeff Kreis
When I was a young boy, I was fascinated with Infocom games like Zork
for my Apple ][e. For anyone out there who may have never played the
game, freeware versions for Windows, DOS, and Mac are available at:
=url http://www.csd.uwo.ca/Infocom/ http://www.csd.uwo.ca/Infocom/
Zork is a text based adventure game where you try to find your way
through a maze by typing text commands such as "look book" (for
looking at a book) or "go north" to move north. The thing that made
the Zork games so interesting, however, was the fact that the actions
you could perform were open ended and depended on where you were. For
instance, you encounter a great cavernous room with a railing
surrounding a pit. You happen to have a rope with you, so you can
"tie rope to railing" or "attach rope to railing" and then you can
"climb down rope" or "climb down". In other rooms these commands
would not do anything.
This type of game play required that Zork have a context dependent
interpreter. Zork needed to be able to interpret a basic set of
commands (movement, inventory, inspection, etc) as well as additional
commands depending on where you happened to be.
We can create just this kind of interpreter fairly easily in REBOL
using parse. Let's make a simple structure that we can navigate and
an interpreter that allows us to roam that space and inspect the
locations.
--- Map
First we need to define a map, and build a few functions for
navigating and describing the map. We'll create a four room
Manor. Conceptually, our Manor will look like the following:
+-----------+-----------+
| 1x1 | 1x2 |
| | |
| Kitchen | Study |
| | |
+----=------\-----=-----+
| 2x1 | 2x2 |
| = |
| Dining | Parlor |
| Room | |
+-----------+-----------+
The characters '=' and '\' are doors. We create the above structure
as a nested set of blocks, one block per row of the Manor, one block
per room in each row. This format allows us to easily address rooms.
the-manor: [
[
["Kitchen" [2x1 2x2] {
The kitchen is hot from the large black iron
stove that occupies the center of the room. The
smell of roasted duck fills the air. Freshly
cut vegetables lie on the cutting block.}
[]
]
["Study" [2x2] {
The study is filled with ancient tomes on the art
of supernatural REBOL scripting. Great REBOL spells
are found on the pages of the parchment}
[]
]
]
[
["Dining Room" [1x1 2x2] {
A long oak table fills this room, covered with green
velvet. Fine pewter dinnerware has been laid out and
each plate has a small card indicating a guest. You see
a seat with your name on it.}
[]
]
["Parlor" [1x2 1x1 2x1] {
Ornate furniture is carefully laid out in the parlor.
Each chair and couch has been placed in such
relations as to encourage light conversation. A
pinball machine is set against the wall next to
a bust of Diogenes.}
[]
]
]
]
Okay, so that's our little Manor. (For now, don't worry about those
extra empty blocks we inserted in each room. We'll use those blocks
later.) We'll also need to define a few variables to keep track of
where we are, blocks describing names of directions and corresponding
pairs, a function to fetch rooms from a pair! and a function to
describe where you can go to, given a room:
get-room: func [loc [pair!] /local x y][
x: loc/x y: loc/y
the-manor/:x/:y
]
dir-name-pair: [
north -1x0 north-east -1x1 north-west -1x-1
south 1x0 south-east 1x1 south-west 1x-1
east 0x1 west 0x-1
]
dir-pair-name: head reverse copy dir-name-pair
exits?: func [room [block!]][
prin "Doors lead to: "
foreach dir room/2 [
dir: dir - loc
prin [select dir-pair-name dir #]
]
print #
]
;-- Loc is where we are
loc: 1x1
current-room: get-room loc
--- REPL
Okay, now we have all we need to start roaming the manor. Now we'll
build our quick and dirty interpreter that allows us to do two things:
1. Move around the map. 2. Look at the rooms. Here's our interpreter:
manor-rules: [
['go | 'move] set where word! (
spot: loc + select dir-name-pair where
either not find current-room/2 spot [
print ["You can't go" where]
][
current-room: get-room loc: spot
]
) |
['look | 'inspect] (print current-room/3 ) |
['q | 'quit] (quit)
]
forever [
print ["You are in the" current-room/1]
exits? current-room
if not parse load ask "=> " [manor-rules][
print "Huh?"
]
]
The section of code contained in the FOREVER statement is known as a
"read-eval print loop" (meaning this is the code responsible for
reading input, evaluating that input, and printing the results). A
read-eval print loop (REPL) is found in all interpreters (including
REBOL), in one form or another.
Our REPL really boils down to a single line:
parse load/all ask "=> " [manor-rules]
The inner ASK takes care of fetching input from the user. The
LOAD/all takes care of splitting our input into separate "tokens" (the
/all refinement makes LOAD always return a block, even if only a
single word is input), and finally, the outer PARSE takes care of
actually interpreting our "token stream". With one line we've created
a fully functioning interpreter, something some people go to grad
school to do!
So here we see our interpreter in action as we start up manor.r and
walk from the kitchen to the study where we look around:
You are in the Kitchen
Doors lead to: south south-east
=> go south-east
You are in the Parlor
Doors lead to: north north-west west
=> go north
You are in the Study
Doors lead to: south
=> look
The study is filled with ancient tomes on the art
of supernatural REBOL scripting. Great REBOL spells
are found on the pages of the mystic parchment.
You are in the Study
Doors lead to: south
=>
Simple enough.
--- Changing the Rules of the Game
Now what we want to do is be able to do different
things inside each room. In the dining room, we want to be able to
sit down at the table, have our meal brought to us and eat. We can't
eat until we've sat down.
We can handle this by having rules that apply to the room we're in.
We'll place our "room-rules" inside those empty blocks in the fourth
position inside each room. Here's our modified dining room:
["Dining Room" [1x1 2x2] {
A long oak table fills this room, covered with green
velvet. Fine pewter dinnerware has been laid out and
each plate has a small card indicating a guest. You see
a seat with your name on it.}
[
'sit opt 'down (
print trim/auto
{You take your seat at the table.
Jeeves brings you your supper.}
room-rules: [
['eat | 'chow ] (print "Yummy. Tastes good!")
]
)
][]
]
See what's happening yet? We are going to have a separate parse rule
for each room, and we can actually change that rule depending on what
happens. We need to do a little tweaking to our REPL, like so:
room-rules: current-room/4
forever [
print ["You are in the" current-room/1]
exits current-room
if not parse load/all ask "=> " [manor-rules | room-rules][
print "Huh?"
]
]
First we set up the ROOM-RULES for when we first start. We add our
ROOM-RULES to our PARSE rules for each interpreted line. We need one
final addition to make this work. In our rule for moving we need to
update the ROOM-RULES based on the room you enter:
['go | 'move] set where word! (
spot: loc + select dir-name-pair where
either not find current-room/2 spot [
print ["You can't go" where]
][
current-room: get-room loc: spot
room-rules: current-room/4
]
) |
Above, when we enter a room, we set the new ROOM-RULES. Okay, so
we'll run our manor program, and go have a meal:
You are in the Kitchen
Doors lead to: south south-east
=> go south
You are in the Dining Room
Doors lead to: north east
=> eat
Huh?
You are in the Dining Room
Doors lead to: north east
=> sit down
You take your seat at the table.
Jeeves brings you your supper.
You are in the Dining Room
Doors lead to: north east
=> eat
Yummy. Tastes good!
You are in the Dining Room
Doors lead to: north east
=>
Notice that we can't eat until we have sat down and Jeeves has brought
us our meal. We can change the rules of the game while we're playing,
which some people would say is cheating, but in REBOL it's just plain
cool.
We'll leave it at that for now. Hopefully it should be apparent how
easily this type framework could be extended. Creating a dynamic
interpreter, as we have done here, is not just applicable to making
games. Many kinds of tasks can be handled with elegance by creating
an interpreter to manipulate aspects of your task.
By creating your own interpreter, you can work with commands that you
define which are the most suitable for expressing your problem. The
power of REBOL dialecting is well demonstrated by the ease at which
you can build custom dynamic interpreters to attack domain specific
problems.
--- The code
Below is the complete Manor.r script. Enjoy your meal!
REBOL [
Title: "The Manor"
Author: "Jeff Kreis"
Purpose: "Demonstrates a custom dynamic interpreter"
]
the-manor: [
[
["Kitchen" [2x1 2x2] {
The kitchen is hot from the large black iron
stove that occupies the center of the room. The
smell of roasted duck fills the air. Freshly
cut vegetables lie on the cutting block.}
[]
]
["Study" [2x2] {
The study is filled with ancient tomes on the art
of supernatural REBOL scripting. Great REBOL spells
are found on the pages of the mystic parchment.}
[]
]
]
[
["Dining Room" [1x1 2x2] {
A long oak table fills this room, covered with green
velvet. Fine pewter dinnerware has been laid out and
each plate has a small card indicating a guest. You see
a seat with your name on it.}
[
'sit opt 'down (
print [
"You take your seat at the table." newline
"Jeeves brings you your supper." newline
]
room-rules: [
['eat | 'chow ] (print "^/Yummy. Tastes good!^/")
]
)
]
]
["Parlor" [1x2 1x1 2x1] {
Ornate furniture is carefully laid out in the parlor.
Each chair and couch has been placed in such
relations as to encourage light conversation. A
pinball machine is set against the wall next to
a bust of Diogenes.}
[]
]
]
]
manor-rules: [
['go | 'move] set where word! (
spot: loc + select dir-name-pair where
either not find current-room/2 spot [
print ["You can't go" where]
][
current-room: get-room loc: spot
room-rules: current-room/4
]
) |
['look | 'inspect] (
print [trim/auto current-room/3 newline]
) |
['q | 'quit] (quit)
]
get-room: func [loc [pair!] /local x y][
x: loc/x y: loc/y
the-manor/:x/:y
]
dir-name-pair: [
north -1x0 north-east -1x1 north-west -1x-1
south 1x0 south-east 1x1 south-west 1x-1
east 0x1 west 0x-1
]
dir-pair-name: head reverse copy dir-name-pair
exits?: func [room [block!]][
prin "Doors lead to: "
foreach dir room/2 [
dir: dir - loc
prin [select dir-pair-name dir #]
]
print #
]
loc: 1x1
current-room: get-room loc
room-rules: current-room/4
;-- REPL
forever [
print ["You are in the" current-room/1]
exits? current-room
if not parse load/all ask "=> " [manor-rules | room-rules][
print "Huh?"
]
]
=== Exposing Rebol CGI through SOAP
by Graham Chiu
As some of you know, getting web content through CGI can be a painful
process. In most situations, the output of the CGI script is in HTML
which is intended for a browser to display as human readable text. If
I want to automate this process, for example to grab weather
forecasts, I can easily write Rebol scripts that strip out all the
tags, images, and javascript, but in most instances I have to write a
new script for each website. Boring!
This problem is one that has clearly plagued many an Internet user,
and there have been a number of attempts in the past to establish a
standardized way to enable applications to access web services
programmatically. SOAP or Simple Object Access Protocol has evolved
from these attempts, and cleverly leverages the existing
infrastructure of HTTP and web servers wrapping messages in XML to
deliver RPC ( remote procedure calls ).
The latest SOAP specification was drafted by UserLand, Ariba, Commerce
One, Compaq, Developmentor, HP, IBM, IONA, Lotus, Microsoft, and SAP,
and with that kind of support, is not likely to disappear overnight!
In order for an application to call a SOAP service, it needs to know
where the service is, what methods are available, what parameters are
used, and what to expect for results. This is done through using a
WSDL file. Web Services Description Language is an XML based language
that was invented for this purpose. Writing this file to describe
your service may be the hardest part of turning an existing Rebol CGI
script into a SOAP service! I took the easy way out and modified a
WSDL file that I found on the web so that it described my service.
For the purposes of this article, I took a script that was posted to
the Rebol mailing list by Johan Forsberg that converts a Gregorian
date to the Paratheo-anametamystikhood calendar of Eris Esoteric. I
have really no idea what this means, but it looked cute and suitable
for the task.
The WSDL file for this service can be found at:
=url http://www.compkarori.com/wsdl/discordian.wsdl http://www.compkarori.com/wsdl/discordian.wsdl
and there is only one method: "discordian"
The CGI script is at:
=url http://www.compkarori.com/cgi-local/discordian.r http://www.compkarori.com/cgi-local/discordian.r
A SOAP client will call this method by posting the following SOAP
message to the above CGI script:
POST /cgi-local/discordian.r HTTP/1.1
Content-Type: text/xml; charset="utf-8"
SOAP-Action:
Content-Length: 715
2001
06
25
As you can see, the header looks very much like a standard POST except
the type is text/xml rather than text/html, and there is the
SOAPAction header which identifies this as a SOAP HTTP request. In
this case the SOAPAction is blank. The SOAPAction header is supposed
to be used to assist in routing messages through firewalls, and to
give a hint to the web server as to what to do with the request
eg. Off load the request to a special server that deals only with SOAP
requests. However, there are moves afoot to deprecate it’s use
altogether.
The effect of the message is to call the discordian.r CGI script with
the method "discordian" and the date 25/6/2001. Note that for clarity
I have shown the message with carriage returns, but a real SOAP
message does not have carriage returns, nor tabs.
The service responds ( again with carriage returns added for clarity ):
Content-type: text/xml; charset="utf-8"
Content-Length: 615
Sweetmorn, Confusion 30, Year of Our Lady of Discord 3167
As you see the result of SOAP call is the message between the
tags. If the
SOAP call is structured incorrectly, or an invalid date is passed, I
need to instead return an error message, and the format for this is
defined by the SOAP specification.
So, to respond to a SOAP request, all my CGI script has to do is:
#Return appropriate headers
#Parse the SOAP message to ensure that it conforms to my script
requirements, and extract out any parameters
#Return either a result, or an error response.
The working CGI script can be viewed at
=url http://www.compkarori.co.nz/reb/discordian.txt http://www.compkarori.co.nz/reb/discordian.txt
To return appropriate headers , I start simply by
print {Content-type: text/xml; charset="utf-8"}
The next line looks like this,
print [ {Content-Length: } length? message {^/} ]
but I can't send it till I construct my message, be it a result or an
error response, as I do not know yet how long it will be.
I need to signal an error if I do not receive the correct payload from
the SOAP request. This could mean that my script has been sent an
invalid date, or that the message has been incorrectly formed. For my
purpose, if I can parse out the year, month and day, I will accept
that as a valid request.
Here's where this occurs in the script:
yearrule: [ thru "" copy ryear to ]
monthrule: [ thru "" copy rmonth to ]
dayrule: [ thru "" copy rday to ]
go: does [
either all [ ( parse query-string [ some yearrule to end ] )
( parse query-string [ some monthrule to end ])
( parse query-string [ some dayrule to end ] ) ]
[ sendResult ]
[ sendError {Incomplete SOAP message} ]
]
I have let the functions 'sendResult and 'sendError print out the
"Content-Length:" header. Note that a blank line follows the header
before the content as in any other http header.
The 'sendError function takes an error message as a parameter, and
inserts it into a pre-made error message. It then removes all the
newlines and replaces them with spaces, and then prints them to back
to the SOAP client. The 'sendResult does a similar thing but returns
a result.
I should also return a HTTP status code of 500, indicating a server
error, if there is a problem with the SOAP request, but since I do not
have any control over this header, I will ignore it and hope that the
SOAP client understands that there was a problem!
So, as you can see, it is not very difficult to turn a Rebol CGI
script into a SOAP service.
PS: If you want to check out this SOAP service using a SOAP client,
you can do it online at
=url http://www.soapclient.com/soaptest.html http://www.soapclient.com/soaptest.html
and where it says "WSDL File Address" enter the URL for my WSDL file
of
http://www.compkarori.com/wsdl/discordian.wsdl
[ This is now listed at http://www.xmethods.com/ -
Note that this is the first Rebol SOAP service publicly
listed :-) ]
Copyright Graham Chiu 2001
gchiu at compkarori dot co dot nz
=== The GENERATE-DATA Dialect
by Joel Neely
Version: 1.2.1
--- Summary
This article describes a simple dialect for automatically
generating text data and the implementation of the engine
that interprets it. The GENERATE-DATA dialect could be
used to construct test data for programs under development,
testing, or evaluation.
In the interest of simplicity and rapid development,
GENERATE-DATA uses a lower-level dialect than that of
PARSE. My strategy was to create an engine that could be
used as-is, but whose dialect could also support future
"compilers" that would translate from higher-level dialects
into the engine's "assembler-like" dialect.
--- Background
This exercise was motivated by a recent discussion on the
REBOL mailing list. My thanks to the list for a constant
flow of interesting ideas and inspiration!
+++ Notations
Programmers have needed to parse and process text since
the dawn of computer programming. The most basic step is
to describe acceptable text formats and be able to decide
whether a given piece of text fits that description. Two
of the most important notations for describing text are
Backus-Naur Form (BNF) and Regular Expressions (RE). Each
of these gives us a way to describe patterns we expect in
acceptable text.
For example, we could define an "int" as being a sequence
of one or more decimal digits. In BNF, we'd write this as:
::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
::= |
(with tail-recursion for repetition). A corresponding
RE would be:
[0-9]+
(where the plus sign means "one or more of the preceding").
Complicating these rules to prevent leading zeroes in the
muliple-digit case is left as an exercise for the reader. ;-)
Both BNF and RE have been extended and modified over the
years. The underlying concepts of BNF can be used to
explain the PARSE dialect of REBOL, as well as the notation
of XML DTDs. A greatly-enhanced version of RE is the basis
for the powerful string processing capabilities of Perl.
However, the fundamentals of both BNF and RE can be applied
in a different way. Instead of reading them as rules for
recognizing or matching existing data, we can view them
as specifications for creating new data strings. That
is the application that inspired the GENERATE-DATA dialect.
+++ GENERATE-DATA
The GENERATE-DATA function is defined in datagen.r, which
is listed at the end of this article and is available
on my RebSite ("jn" on the View Sites display).
GENERATE-DATA takes one argument, a rule which describes
the possibilities for the data to be generated. The next
two sections describe the dialect used to write those rules.
The remainder of the article is about the GENERATE-DATA
engine itself.
--- Basic Rule Types
The simplest possible specification would just list all of
the legal strings. A more efficient approach is to specify
some minimum number of strings or characters, and then
write construction rules that tell how to build additional
strings from the "raw materials" already specified.
Experience with almost any variation of BNF or RE suggests
a small list of basic rule types that can be combined to
specify text very effectively. If our dialect can express
rules for literal values, sequences of rules, choices
between rules, repeating rules, and recursion, we'll have
more than enough building blocks to construct interesting
collections of text.
The first rule type allows us to state legal strings.
All of the other kinds of rules describe how to construct
valid data from other rules or data. Our dialect will
specify each of these rule with a block, where the first
element is a word that identifies the type of rule.
+++ Literal Value Rules
If we have a rule that says that
"glorp"
is a legal string, then we can create a legal string by
producing a copy of that value. Evaluating
>> generate-data "glorp"
== "glorp"
asks the engine to give us an instance of that string.
+++ Sequence Rules
We often need to build up a data value sequentially, by
specifying each part in order. We ask the generator
to make a sequence of values by a rule that looks like
[seq ...parts in order...]
The parts are concatenated in the order specified, with
no additional data (such as spaces) inserted between
them. Since we've only covered literal values thus far,
we can only write expressions like
>> generate-data [seq "John" " " "Q." " " "Public"]
== "John Q. Public"
which is the long way around compared with using the
literal rule
>> generate-data "John Q. Public"
== "John Q. Public"
It's clear that our data will get VERY boring VERY
quickly unless we can add more options.
+++ Choice Rules
Speaking of options, how about selecting a value from a
collection of alternatives? Using an abbreviation of the
word "alternatives" as our mnemonic, we'll write a choice
rule as
[alt ...alternatives...]
so that evaluating the expression
>> generate-data [alt "fee" "fie" "foe" "fum"]
== "foe"
asks the generator to give us back exactly one of those
strings, selected at random.
Thinking back to our "digit" example, we could ask for
a single digit by evaluating
>> generate-data [alt "0" "1" "2" "3" "4" "5" "6" "7" "8" "9"]
== "3"
Building up data with this character-by-character approach
would be very tedious, so we'll make a shortcut for
character-level options. This format
[char ...some-string...]
asks the generator to pick one of the characters from the
string. We can ask for a random decimal digit as follows:
>> generate-data [char "0123456789"]
== "4"
Now we can write a more interesting rule, which allows us
to vary the middle initial of a made-up name.
>> generate-data [seq
[ "John " [char "ABCDEFGHIJKLMNOPQRSTUVWXYZ"] ". Public"
[ ]
== "John K. Public"
+++ Repetition Rules
We need a rule that asks for some other rule to be applied
repeatedly. We specify lower and upper limits for how many
times to repeat, and the generator makes a random selection
between those limits. The format of this rule is
[rep ...lolimit...hilimit...other-rule...]
Now we can describe that annoying guy behind us at the
traffic light by evaluating
>> generate-data [rep 4 8 "Honk! "]
== "Honk! Honk! Honk! Honk! Honk! "
which will generate at least four, but no more than eight,
honks in a row.
+++ An Example of "Normal Integers"
Let's finish the discussion of basic rules by returning to
the "normally-written integers" example from the background
section. A normal integer can have one digit (which can
be any decimal digit) or multiple digits (beginning with a
non-zero digit, which is followed by one or more other digits
which may be zero).
To keep the generator from running away, let's limit the
result to a total of seven digits. A complete expression for
this verbal description would be
>> generate-data [alt
[ [char "0123456789"]
[ [seq
[ [char "123456789"]
[ [rep 1 6 [char "0123456789"]]
[ ]
[ ]
== "888655"
This works, but I'd like to build rules with fewer keystrokes
than that. We'll talk about ways to do that in the next section.
--- Additional Features
We can increase the convenience of this little dialect by
adding the ability to name rules, both for documentation
and for re-use. Even more expressive power comes from
being able to evaluate additional REBOL expressions during
the generation process.
+++ Names for Rules and Data
That last example (normally-written integers) would be more
readable if we could
*give descriptive names to nested rules, and
*avoid repeating the same literal strings.
The GENERATE-DATA dialect allows rules to include words that
refer to other rules (or parts of rules). We can rewrite
the previous example as
nzdigit: [char "123456789"]
anydigit: [char "0123456789"]
generate-data [alt
anydigit
[seq nzdigit [rep 0 6 anydigit]]
]
to save keystrokes and improve readability.
Rule names can even be used recursively (but be sure to
leave an escape case!) Suppose we have a game based on
flipping a coin. If the coin comes up heads, the game
is over. If the coin comes up tails, we keep playing.
This rule
coin-game: [alt
"head "
[seq "tail " coingame]
]
produces the following results
>> generate-data coin-game
== "tail head"
>> generate-data coin-game
== "head"
>> generate-data coin-game
== "tail tail tail tail tail head"
In addition to containing an entire rule, a word can be used
to supply the string value for a character-selection rule,
a block of values for a alternatives rule, or either of the
limits for a repetition rule. We can generate polite names
for "random" individuals with this set of definitions
how-many: 4
title-suffix: "rs"
last-names: ["Martin" "Nitsch" "Kamp" "Kreis" "Cole"]
polite-name: [seq
"M" [char title-suffix] ". "
[alt last-names] " "
]
which work this way
>> generate-data [rep how-many how-many polite-name]
== "Ms. Martin Ms. Kamp Mr. Nitsch Ms. Martin "
In this example, we set the lower and upper bounds of the
repetition to be equal, ensuring that we'll get the same
number of names each time we evaluate that last rule.
+++ Predefined Names
Some rules are so common that it's handy to have them
built into the engine. The current version (1.2.2) has
the following rules already defined:
:digit - any decimal digit, shorthand for [char "0123456789"]
:letter - any lowercase (English) letter, shorthand for
[char "abcdefghijklmnopqrstuvwxyz"]
:space - single blank character
:newline - platform-dependent line termination
:tab - horizontal tab
We'll use all of these additional features to write some rules
that manufacture random REBOL code (another topic recently
mentioned -- just as a joke! -- on the REBOL mailing list).
These rules
word-name: [seq letter [rep 0 4 [alt letter digit]]]
word-setter: [seq word-name ":"]
number: [rep 1 3 digit]
term: [alt word-name number]
arith-op: [alt "+" "-" "*" "/"]
arith-expr: [seq term [rep 0 3 [seq " " arith-op " " term]]]
set-expr: [seq word-setter " " arith-expr]
print-expr: [seq "print " word-name]
rand-expr: [alt set-expr print-expr]
indenting: " "
rand-block: [seq
"[" newline
[rep 2 5 [seq indenting rand-expr newline]]
"]" newline
]
make up a random-REBOL-writing robot!
>> print generate-data rand-block
[
ac83: n755d + q9t45
az: 1 / y + k - 00
]
We'll call our dialect description complete after describing one
last handy feature.
+++ Embedded REBOL
To add the final customization touch, let's extend the dialect
to include REBOL expressions in parentheses. These expressions
are evaluated during generation, and the resulting values are
inserted into the generated text at that point. Simple, non-string
values, such as characters, integers, and times, are converted to
strings before insertion.
This feature allows us to make up even more realistically-appearing
data. For example, suppose we want to generate a data file that
resembles a log from some internet application. Each tab-delimited
line in the log should contain a sequence number, a time of day,
a count of bytes transferred, and a URL. We can manufacture this
sample data file with the following rules.
counter: 0
timestamp: now/time
min-lines: 10
max-lines: 15
hosts: [
"www.foo.com"
"www.gloop.org"
"www.hairy.net"
]
pages: [
""
"people.html"
"places.html"
"things.html"
"search.html"
]
log-line: [seq
(counter: counter + 1) tab
(timestamp: timestamp + random 15) tab
(500 + random 1500) tab
"http://" [alt hosts] "/" [alt pages] newline
]
log-file: [seq
(counter: 0 timestamp: 12:00:00 "")
[rep min-lines max-lines log-line]
]
Evaluating the expression
write %dummylog.txt generate-data log-file
creates a data file resembling the following.
1 12:00:02 1939 http://www.foo.com/places.html
2 12:00:05 1650 http://www.foo.com/things.html
3 12:00:20 862 http://www.foo.com/things.html
4 12:00:23 609 http://www.gloop.org/search.html
5 12:00:37 1183 http://www.foo.com/things.html
6 12:00:52 1989 http://www.hairy.net/places.html
7 12:00:55 1267 http://www.gloop.org/things.html
8 12:00:57 1604 http://www.gloop.org/things.html
9 12:01:09 995 http://www.foo.com/places.html
10 12:01:18 1955 http://www.hairy.net/places.html
11 12:01:24 1173 http://www.hairy.net/things.html
12 12:01:36 1906 http://www.hairy.net/search.html
13 12:01:39 1905 http://www.foo.com/things.html
14 12:01:51 1407 http://www.gloop.org/places.html
15 12:02:04 1948 http://www.foo.com/search.html
Notice the paren expression at the beginning of LOG-FILE
that resets the COUNTER and TIMESTAMP values, so that they
start over every time the LOG-FILE rule is used. The last
value in the parentheses returns an empty string so that
the output isn't disturbed.
--- The GENERATE-DATA Engine
Without further ado, here is the engine that implements this
dialect. A few key points are described after the listing.
+++ The Code
REBOL [
Title: "GENERATE-DATE Dialect Engine"
Author: "Joel Neely"
Date: 29-June-2001
Version: 1.2.1
File: %datagen.r
Comment: {Documented in RebZine #2!}
]
;
; protect global namespace
;
datagen: make object! [
;
; standard character classes -- could be expanded
;
digits: "0123456789"
letters: "abcdefghijklmnopqrstuvwxyz"
;
; resolve words and convert to appropriate types
;
deref: func [item] [either word? item [get item] [item]]
get-block: func [item] [ deref item]
get-abs: func [item] [ abs deref item]
get-string: func [item] [to-string deref item]
;
; generate an alternative (allowing word for choices)
;
genalt: func [b [block!]] [
gendata random/only
either all [
1 = length? b
any [paren? b/1 path? b/1 word? b/1]
] [get-block b/1] [b]
]
;
; generate a character choice
;
genchar: func [s [string!]] [to-string random/only s]
;
; generate a repetition (including zero counts)
;
genrep: func [lo [integer!] hi [integer!] spec /local r] [
set [lo hi] reduce [min lo hi max lo hi]
r: copy ""
loop lo - 1 + random hi - lo + 1 [append r gendata spec]
r
]
;
; generate a sequence
;
genseq: func [b [block!] /local i r] [
r: copy ""
foreach i b [append r gendata :i]
r
]
;
; control the overall generation process
;
gendata: func [spec] [
if error? try [spec: spec] [spec: ""]
any [
if block? spec [
switch/default spec/1 [
alt [genalt next spec]
char [genchar get-string spec/2]
seq [genseq next spec]
rep [genrep get-abs spec/2 get-abs spec/3 spec/4]
][ rejoin ["*** unknown verb '" mold spec "' ***"]]
]
if word? spec [
switch/default spec [
digit [genchar digits]
letter [genchar letters]
space [" "]
newline newline
tab tab
][ gendata get spec]
]
to-string spec
]
]
]
;
; expose one global function name
;
generate-data: func [spec [block! string!]] [datagen/gendata spec]
+++ Remarks
The DATAGEN/DEREF function fetches the value for any non-dialect
word used inside any rules. The following GET-xxx routines
use it in preparation for converting values to the appropriate
type, based on where those values are to be used.
The DATAGEN/GENALT function handles the ALT rule format. The word
ALT may be followed by an explicit set of alternatives, or it may
be follow by a single word that supplies the list of options. Once
that notational choice is handled, RANDOM/ONLY selects a single
option to be interpreted as a rule itself.
DATAGEN/GENREP repeatedly calls for its internal specification to
be interpreted, appending each of those results into a final
response. The interesting part is setting the repetition count.
First, we ensure that the LO and HI boundaries are actually in
order. The complicated-looking expression for the LOOP count can
best be understood by range arithmetic. If HI equals LO plus
some WIDTH (which is zero or greater), then HI - LO + 1 is equal to
WIDTH + 1 (which will be one or greater). Therefore, RANDOM WIDTH + 1
must be in the range [1 ... WIDTH + 1]. Adding that range to
LO - 1 gives a result in the range [LO - 1 + 1 ... LO - 1 + WIDTH + 1],
which simplifies to [LO ... HI]. This effort is require because
RANDOM prefers to start a 1 and we must allow for equal lower and
higher limits.
DATAGEN/GENSEQ accumulates its answer by stepping through the
its argument block, which is the list of rules after SEQ.
DATAGEN/GENDATA is the main "control center" of the engine. The
line
if error? try [spec: spec] [spec: ""]
deals with resolving paren and path values, and also ensures that
expressions without values (such as PRINT) are treated as empty
strings instead of errors.
The remainder of the function determines whether
*to handle a rule block with its own generating function
(converting portions of the block to the required types), or
*to attempt to resolve a dialect or external word, or
*to simply convert the spec to a string.
That last action allow us to use other REBOL types (such as
characters, integers, or times) as literal rules in our dialect.
+++ What's Next?
Possible future extensions include
*expanding the definition of LETTERS for our non-English-based friends,
*allowing BITSET! values as an alternative to the CHAR rule,
*adding wrappers which translate other dialects down to the basic
GENERATE-DATA dialect/engine, and
*your own ideas!
I already have some everyday uses for this dialect. I hope it will
be useful to some of you as well!
-jn-
joel dot neely at fedex dot com
=== All the Mondays in a Year
by Garth Rebola
Ports in REBOL are a type of series abstraction. Most of the time
ports are used to abstract Internet protocols, but the design of ports
is more generic and accommodating. Below I define a port which
manages a date. This date:// scheme creates ports which accepts a
dialect that is inserted into the port. The dialect changes the state
of the port so that picks on that port will return different date
information based on where in the series you are picking. Sounds odd,
but here's the code and then we'll see how it works afterwards:
REBOL [
Title: "Date-port"
]
make object! [
port-flags: system/standard/port-flags/pass-thru
init: func [port spec][
if url? spec [net-utils/url-parser/parse-url port spec]
port/state/flags: port/state/flags or port-flags
port/state/index: 0
port/state/tail: 365
]
open: func [port][
port/locals: make object! [
start: now/date
skipit: 1
ret-type: 'weekday ; or 'day 'date '
parser: func [port data /local t][
parse data [
some [
'start set start! date! |
'skip set skipit integer! |
'return set ret-type word! |
'tail set t integer! (port/state/tail: t)
]
]
]
pickit: func [index /local day][
day: start + (skipit * index)
switch ret-type [
weekday [
system/words/pick system/locale/days day/weekday
]
day [day/weekday]
date [day]
year [day/year]
]
]
]
error? try [if port/host [port/locals/start: to-date port/host]]
error? try [
if port/target [
port/locals/ret-type: to-word port/target]
]
]
insert: func [port data][
either date? data [
port/locals/start: data
][ if block? data [port/locals/parser port data] ]
]
pick: func [port][
port/locals/pickit port/state/index
]
net-utils/net-install date self 0
]
Okay, so now we can open a date port:
date-port: open date://
By default our date port is initialized to today and it will return
the day of the week when you pick the port:
print [
"Today is:" weekday: pick date-port 1
]
We can see what day of the week is the third day in our series (two
days from today, the first day in the series).
print [
"Two days from today will be:" pick date-port 3
]
Fun stuff. Now we can do more, though. We can setup our port so that
we skip forward a week at a time each pick we make. We can also
return the date instead of the weekday:
insert date-port [skip 7 return date]
By inserting our dialect into the port we have changed the state of
the port. Now we can see what date it will be 10 weeks from now.
print rejoin [
"10 " weekday "s from today will be: "
pick date-port 11
]
Okay, well we happen to know that 1-Jan-2001 was a Monday. So we want
to print out all the Mondays in the year, so we can schedule to be
spaced out on those days. First we open a date-port on 1-Jan-2001,
with date as the return type:
date-port: open date://1-Jan-2001/date
Now we tell our port we want to skip forward 7 days at a time and the
end of the port is 52 (52 weeks in a year).
insert date-port [skip 7 tail 52]
And we just FORALL over our Mondays and see our happy day list:
print "These are the mondays of 2001:"
forall date-port [print first date-port]
Have fun!
-Garth
=== Rebbots - REBOL email robots
By Chris
Updating a news section on a website can be a pain. You have to sit
down with your editor, fiddle with the html to insert your new item,
then mess with an ftp tool to upload your edited page. You can make
life a bit easier by using various cut and paste tricks, but there
is an alternative: the email robot.
With a robot, all you need to do is send the new news entry to an
email address in your domain: the robot takes care of updating the
news page for you! With a little bit of extra effort you can use the
same mechanism to automate all manner of mundane website management
operations.
--- First things first - What is an email robot?
An email robot is a script invoked by the mail software at your hoster
when an email is sent to a specific address. Usually this is done by
setting up a forwarder which pipes the email to the script. For
example, you could set up
rebbot@yourdomain.com -> |/home/yourdomain/rebbot.r
Now any emails sent to rebbot@yourdomain.com invoke rebbot.r. The
contents of the email are passed to the script on the standard input.
If you intend to create an email robot you need to confirm with your
hoster that your forwarders can direct mail to a script. Some hosters
only allow addresses to be redirected to other addresses, some may
require manual setup by the tech support team. Check before you start,
you may waste time and effort otherwise!
--- The most basic email rebbot
This rebbot is virtually useless - all it does is read from stdin and
save the data to a file. However, it is a good idea to try this rebbot
out first, just to make sure you have set the forwarder up correctly.
#!/path/to/rebol -qsw
REBOL [ ]
buffer: make string! 15000
read-io system/ports/input buffer 15000
write %robotoutput.txt buffer
Once you have your forwarder set up, try sending an email to it. If
everything is working properly, robotoutput.txt should contain the
email you sent. If something goes wrong you will get an error message
from the mail software at your hoster.
--- Things to think about next
Before you can go much further you need to give thought to security.
All the security issues surrounding cgi scripts - buffer overflows,
authorizing access etc - apply to email Rebbots. Anyone who uncovers
the robot address can send email to it, so you need to design your
scripts with that in mind. You may have noticed that I used read-io
with a read length of 15000 bytes in the above example, this technique
ensures that you are not vulnerable to buffer overflow attacks. You
should also validate whether the sender has permission to use the
script. I will show a basic access validation scheme based on the from:
address. This can be faked, but it is a starting point. A more secure
test could use the sender IP from the header, but that is complicated
by the use of dynamic IPs by most dialup ISPs. The ramifications of
the security issues are too great to discuss here, just be careful!
--- Looking at the email
In the above robot example, the email was read into a variable called
buffer. An example of the contents of buffer would be something like:
From chris@starforge.co.uk Tue Jun 19 12:27:53 2001
Received: from anchor-post-30.mail.demon.net ([194.217.242.88])
by wolverine.uk.co with esmtp (Exim 3.20 #1)
id 15COM5-00036G-00
for rim@starforge.co.uk; Tue, 19 Jun 2001 12:27:53 -0400
Received: from starforge.demon.co.uk ([158.152.179.4])
by anchor-post-30.mail.demon.net with smtp (Exim 2.12 #1)
id 15COOQ-000PV6-0U
for rim@starforge.co.uk; Tue, 19 Jun 2001 17:30:19 +0100
From: Chris
Reply-To: Chris
To: ........
Date: Tue, 19 Jun 2001 17:25:21 +0000
Message-ID:
Organization: I'd tell you, but then I'd have to kill you
Subject: testing
MIME-Version: 1.0
Content-Type: text/plain
[ "command" {Command specific text} ]
Actually pulling the important data out of this is very easy in
REBOL. To extract the From: and Subject: lines, all you need to do is
a bit of work with parse. Parsing isn't my strong point, so it may
be possible to do these with one parse call, but anyway...
>> parse buffer [thru "From: " copy email-sender to newline]
>> parse buffer [thru "Subject: " copy email-subject to newline]
Now email-sender will contain "Chris " and
email-subject will contain "testing". To get at the body you need
to either check the email RFCs, or take my word for it that two
newlines indicate the end of an email header. Using this fact, you
can find the start of the body quite easily:
header-end: find buffer "^/^/"
either header-end [
body: at header-end 3
; .. process the body in some way
][
; .. email the sender indicating that the body could not be found
]
The "at header-end 3" is required to look at the start of the body
rather than the first newline at the end of the header. body will
contain {[ "command" {Command specific text}] }. You may want to
use to-block to convert this to something more usable:
>> body: do body
>> print body/1
command
>> print body/2
Command specific text
--- A small aside
A frighteningly powerful, but incredibly neat, REBOL trick must be
mentioned here. Imagine that the following is sent in the body of
an email:
["testing" {Command specific text} [print "hello world"]]
After processing the email as discussed above, we can do
>> do body/3
hello world
Yes, you can - if you are brave (or crazy) enough - allow REBOL
scripts to be sent in the mail to the rebbot to be executed on the
server. Now that I have caused a number of heart attacks among
security sensitive sys admins I should point out that this is a
seriously Bad Idea: if a malicious user managed to work out how to
gain access to, and use, your rebbot who knows what damage this could
cause? Especially as REBOL is being invoked without any security
restrictions! DON'T DO IT unless you want trouble!
Which is a shame ;)
--- Putting it all together
This is a small script which demonstrates the basic structure a rebbot
could take. As always, TMTOWTDI, but this is a basic starting point..
#!/path/to/rebol -qsw
REBOL []
; Read stdin - restricts emails to 15k, this may or may not be a problem..
buffer: make string! 15000
read-io system/ports/input buffer 15000
; obtain sender and subject fields
parse buffer [thru "From: " copy email-sender to newline]
parse buffer [thru "Subject: " copy email-subject to newline]
either email-sender [
; I don't reply to unauthorized users.
; A Dummy bounce could be a good response though
; validate-sender could look up authorized senders
; from a file for example.
either not validate-sender email-sender [
; add-log should write the email to a file
; along with an error message
add-log/accesserror buffer
][
; Find the body
body: find buffer "^/^/"
either body [
body: at body 3
either error? body: do body [
disarm body
add-log/badbody buffer
; send an email to the user with an error message
][
; This function should act on the
; contents of body in some way
process-command body
]
][
add-log/nobody buffer
; You will probably want to email
; the sender here with an error
]
]
][
; not sure if this can happen, best to make sure..
add-log/nofrom buffer
]
This leaves you a fair amount to work out - add-log, validate-user and
process-command in particular. You may be wondering why I've also
avoided the email lines when REBOL has a perfectly good "send"
command. Unfortunately, send only works when your hoster has an SMTP
server running. Even then, sending can be more complicated than
just using "send". If your hoster doesn't even have an SMTP server you
need to write something to invoke sendmail. To do that... well,
perhaps another time. :)
-Chris
chris at starforge dot co dot uk
=== Mental Reduction
by Carl Sassenrath
If you are new to REBOL, learn how to reduce your expression
complexity by removing common subexpressions. It can save typing,
plus it will save time later when you maintain your code because
you have a lot less code to maintain.
Someone recently sent me the expression below. It's a clear case
for showing the benefits of mental reduction. Take a look:
either (mode) [
data: find data "Active"
][
data: find data "Passive"
]
First, recognize is that EITHER is a function that returns the
result of either block. So, setting the DATA variable can be
done in one place:
data: either (mode) [
find data "Active"
][
find data "Passive"
]
Next, notice that the FIND expression is nearly the same in both
blocks. Only one argument is different, so you can pull FIND
out of the blocks. That leaves just:
data: find data either (mode) [
"Active"
][
"Passive"
]
Now you can collapse the expression into a single line:
data: find data either (mode) ["Active"]["Passive"]
Time to eliminate the extra C-ish parens. Use parens to clarify
an expression and control evaluation order, but don't use them
just for style because they cost you a minor bit of speed in the
interpreter. The line is now:
data: find data either mode ["Active"]["Passive"]
Finally, notice that this use of EITHER is simply a PICK. This is
a hint that you can eliminate one of the blocks. The final
expression becomes:
data: find data pick ["Active" "Passive"] mode
Whenever I write code, I always allocate a few brain cells to
watch for these mental reductions. Often you don't notice them
until you've made the first pass over the expression. But, I
urge you to go back and do the reduction.
Like English, write and rewrite until you have a finely polished
expression that reflects like a mirror in the screenlight.
Later, you'll be glad you did. And, perhaps more importantly,
it gives you beer bragging rights with your Java friends:
Took you just one line. ;)
=== Get-Word!
Ha! A final word! Well, send in your contributions to
rebolzine@yahoo.com. You might notice that the formatting has become
fully make-spec. This makes the most sense. The formatting is:
=== Title
--- subsection
+++ subsubsection
Indent code samples with 4 spaces -- see make-doc.r for more
formatting options. Yehaw.. make-doc will make it much easier to
assemble that way, and publish to web. The first Zine was all over
the map but hopefully it will settle into a sane format as we go
along. Also, if you want your last name included send it along.
Unfortunately, I did not get the last name of Chris (Chris at
starforge dot co dot uk) and I couldn't find it dredging my mail
files. It's okay if an author wants to go by their first name, just
want to give credit for a job well done!
Boy this Zine is big in size. Hope it doesn't bounce all over the
place. Yikes! Well, be good to yourselves!
halt
###