Originally published in Rebol Forces.

Reb-IT!
Volume 1 Issue 2
Date: 29-Jun-2001
Issues: prev | next

Contents

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:

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:

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:

http://www.compkarori.com/wsdl/discordian.wsdl

and there is only one method: "discordian"

The CGI script is at:

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
 
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV=http://schemas.xmlsoap.org/soap/envelope/
xmlns:tns=http://www.compkarori.com/wsdl/discordian.wsdl
xmlns:xsd=http://www.w3.org/1999/XMLSchema
xmlns:soap=http://schemas.xmlsoap.org/wsdl/soap/
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body><mns:discordian xmlns:mns="http://tempuri.org/message/">
<year xsi:type="xsd:int">2001</year>
<month xsi:type="xsd:int">06</month>
<day xsi:type="xsd:int">25</day>
</mns:discordian>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

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
 
<?xml version="1.0" encoding='UTF-8'?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/1999/XMLSchema"
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<NS1:DiscordianResponse xmlns:NS1="http://tempuri.org/message/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<NS1:return xsi:type="xsd:string">
Sweetmorn, Confusion 30, Year of Our Lady of Discord 3167
</NS1:return>
</NS1:DiscordianResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

As you see the result of SOAP call is the message between the <NS1:return xsi:type=”xsd:string”> </NS1:return> 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:

  1. Return appropriate headers
  2. Parse the SOAP message to ensure that it conforms to my script requirements, and extract out any parameters
  3. Return either a result, or an error response.

The working CGI script can be viewed at

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 "<year" thru ">" copy ryear to </year> ]
monthrule: [ thru "<month" thru ">" copy rmonth to </month> ]
dayrule: [ thru "<day" thru ">" copy rday to </day> ]
 
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

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:

<digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
<int> ::= <digit> | <digit> <int>

(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 <chris@starforge.co.uk>
Reply-To: Chris <chris@starforge.co.uk>
To: ........
Date: Tue, 19 Jun 2001 17:25:21 +0000
Message-ID: <yam8570.1081.1195679728@post.demon.co.uk>
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 <chris@starforge.co.uk>" 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