#!/usr/local/bin/rebol -c
REBOL [
Title: "QuarterMaster"
Author: "Christopher Ross-Gill"
Version: 0.3.11
Notes: {Warning: Work-In-Progress - no liabilities for damage, etc.}
License: http://creativecommons.org/licenses/by-sa/3.0/
]
config: construct [
public-key: "a key that will be visible - for cookie ids"
private-key: "a private key - for encrypting passwords"
session-timeout: 0:02:00
zone: -6:00
post-limit: 500 ;-- not active yet; can be altered per controller/action
default-controller: "blog"
spaces: [
;-- QM requires entries for "system" "data" "site" "support"
"system" %/Path/To/app/application/
"space" %/Path/To/app/space/
"site" %/Path/To/HTTPD/root/
"support" %/Path/To/support/
;-- Add more for your convenience
]
]
;--## APPLICATION BEYOND THIS POINT
;-------------------------------------------------------------------##
system/options/binary-base: 64
system/error/user/type: "QuarterMaster Error"
date: now - now/zone + config/zone
range!: :pair! ; until REBOL v3
else: #[true] ; for 'case statements
random/seed to-integer checksum/secure form now/precise ; any better?
if all [debug: find/tail/last any [system/options/cgi/query-string ""] "&debug=" debug: tail? debug] [
print "Content-Type: text/plain; charset=utf-8^/"
]
;--## APPLICATION NAMESPACE
;-------------------------------------------------------------------##
qm: context [
binding: 'self
controller: metadata: action: #[none]
models: request: response: #[none]
alerts: []
errors: []
notices: []
handler: %.rsp
view-path: #[none]
title: ""
code: []
]
;--## EXTENDED CORE FUNCTIONS
;-------------------------------------------------------------------##
context [
func: make function! [spec [block!] body [block!]][make function! spec body]
does: func [body [block!]][make function! [] body]
uses: func [proto [block!] spec [block!]][
proto: context proto
func [args [block! object!]] compose/only [
args: make (proto) args
do bind (spec) args
]
]
try-else: func [[throw] 'try-first [any-block!] on-fail [block!] /local reason][
either error? reason: try :try-first bind :on-fail 'reason [:reason]
]
assert-all: func [[throw] cases [block!] /local value][
until [
set [value cases] do/next cases
unless value cases/1
cases: next cases
any [not value tail? cases]
]
any [value]
]
; step-through: try-each: func ['steps [block!] else [block!] /local reason][
; foreach [block err-code] steps [
; either error? reason: try :block [do bind else 'reason return err-code][:reason]
; ]
; ]
fortype: func [[throw] type [datatype!] block [block!] f [any-function!] /local val][
parse block [some [to type set val type (f :val)]]
]
with: func [object [any-word! object! port!] block [any-block!] /only][
block: bind block object
either only [block] :block
]
envelope: func [val [any-type!]][either any-block? val [val][reduce [val]]]
raise: func [[catch] reason][throw make error! rejoin envelope reason]
export: func [words [word! block!] /to dest [object!] /local word][
dest: any [dest system/words]
fortype word! to-block words func [word] [
set/any in dest word get/any word
; protect in dest word
]
]
true?: func [test][either test [#[true]][#[false]]]
export [func does uses try-else assert-all fortype export envelope raise with true?]
]
;--## SERIES HELPERS
;-------------------------------------------------------------------##
context [
push: func [stack [series! port!] value [any-type!] /only][
head either only [insert/only stack :value][insert stack :value]
]
pop: take: func [series [series! port! none!] /last /part range [integer!] /local result][
range: any [range 1]
case [
none? series [return none]
last [series: skip tail series negate abs range]
]
either part [
result: copy/part series range
remove/part series range
][
result: pick series 1
remove series
]
result
]
pop*: func [stack [series! port!] /local val][
val: pick stack 1
remove stack
:val
]
flatten: func [block [any-block!] /once][
once: either once [
[(block: insert block pop block)]
][
[(insert block pop block)]
]
parse block [
any [block: any-block! (insert block pop block) :block | skip]
]
head block
]
map: func [series [any-block! port!] action [any-function!] /only /copy /local new][
if copy [series: system/words/copy/deep series]
while [not tail? series][
series: either only [
change/part/only series action series/1 1
][
change/part series action series/1 1
]
]
head series
]
map-each: func [[catch throw] 'word [word! block!] series [any-block!] body [block!] /copy /local new][
case/all [
word? word [word: envelope word]
not parse word [some word!][
raise "WORDS argument should be a word or block of words"
]
copy [series: system/words/copy/deep series]
]
use word compose/deep [
while [not tail? series][
set [(word)] series (body)
series: change/part series reduce [(word)] (length? word)
]
]
head series
]
get-choice: func [word [string! word!] words [any-block!]][
all [
word: attempt [to-word word]
find words word
word
]
]
get-class: func [classes [block!] item /local type][
all [
type: type? classes/1
classes: find classes item
first find/reverse classes type
]
]
link-to: func ['path [any-block!] /local out][
out: copy %""
path: compose to-block path
foreach val path [
either issue? val [append out mold val][repend out ["/" form val]]
]
]
compose-path: func ['path [path! lit-path! word! lit-word!]][
to-path new-line/all compose to-block envelope path none
]
paginate: func [series [series! port!] page [integer! none!] /window padding /size length][
page: any [page 1]
length: any [length 15]
padding: any [padding 2]
context [
last: max 1 to-integer (length? series) - 1 / length + 1
current: max 1 min last page
next: either last > current [current + 1][false]
previous: either 1 < current [current - 1][false]
records: copy/part skip series offset: current - 1 * length length
upper: copy [] lower: copy []
repeat cnt padding [
insert lower current - cnt
append upper current + cnt
]
remove-each val lower [val <= 1]
remove-each val upper [val >= last]
start: current - 2 <= padding
end: last - current - 1 <= padding
]
]
prepare: use [rule flat nest val][
rule: [val: paren! (do val/1 remove val) :val | get-word! (change/part val get/any val/1 1)]
flat: [some [rule | skip]]
nest: [some [rule | into nest | skip]]
func [block [any-block!] /deep][
parse block: copy/deep block either deep [nest][flat]
block
]
]
export [push take pop flatten map map-each get-choice get-class compose-path prepare link-to paginate]
]
;--## KEY-VALUE HELPERS
;-------------------------------------------------------------------##
context [
add-to: func [ser key val][
key: envelope key
map key func [key][as word! key]
if find key none! [return none]
until [
ser: any [
find/tail ser key/1
insert tail ser key/1
]
key: next key
switch type?/word ser/1 [
none! [unless tail? key [insert/only ser ser: copy []]]
string! [change/only ser ser: envelope ser/1]
block! [ser: ser/1]
]
if tail? key [append ser val]
]
]
get-from: func [series 'key][
key: copy envelope key
while [all [not tail? key any-block? series]][
series: select series pop key
]
all [tail? key series]
]
export [add-to get-from]
]
;--## CHARACTER SETS
;-------------------------------------------------------------------##
context [
comment {
Will consider the possibility of using more bnf friendly words...
[
snip: difference charset [#"^(20)" - #"^(7F)"] charset [{:*.<>=} #"{" #"}"]
chars-n: charset [#"0" - #"9"] ; digit
chars-la: charset [#"a" - #"z"] ; lower-alpha
chars-ua: charset [#"A" - #"Z"] ; upper-alpha
chars-a: union chars-la chars-ua ; alpha
chars-an: union chars-a chars-n ; alphanumeric
chars-hx: union chars-n charset [#"A" - #"F" #"a" - #"f"] ; hexdig
chars-ud: union chars-an charset "*-._!~'," ; url decode
chars-u: union chars-ud charset ":+%&=?" ; url
chars-f: union chars-an charset "-_" ; file
chars-w1: union chars-a charset "*-._!+?&|"
chars-w*: union chars-w1 chars-n
chars-p: union chars-an charset "-_!+%" ; path
chars-sp: charset " ^-" ; space
chars-as: charset ["^/^-" #"^(20)" - #"^(7F)"] ; ascii
chars-up: charset [#"^(80)" - #"^(FF)"] ; above ascii
; chars-ht: exclude union chars-as chars-up charset {&<>"}
chars-ht: exclude chars-as charset {&<>"}
chars: complement nochar: charset " ^-^/"
]
}
chars-n: #[bitset! 64#{AAAAAAAA/wMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=}]
chars-la: #[bitset! 64#{AAAAAAAAAAAAAAAA/v//BwAAAAAAAAAAAAAAAAAAAAA=}]
chars-ua: #[bitset! 64#{AAAAAAAAAAD+//8HAAAAAAAAAAAAAAAAAAAAAAAAAAA=}]
chars-a: #[bitset! 64#{AAAAAAAAAAD+//8H/v//BwAAAAAAAAAAAAAAAAAAAAA=}]
chars-an: #[bitset! 64#{AAAAAAAA/wP+//8H/v//BwAAAAAAAAAAAAAAAAAAAAA=}]
chars-hx: #[bitset! 64#{AAAAAAAA/wN+AAAAfgAAAAAAAAAAAAAAAAAAAAAAAAA=}]
chars-ud: #[bitset! 64#{AAAAAIJ0/wP+//+H/v//RwAAAAAAAAAAAAAAAAAAAAA=}]
chars-u: #[bitset! 64#{AAAAAKJ8/wf+//+H/v//RwAAAAAAAAAAAAAAAAAAAAA=}]
chars-f: #[bitset! 64#{AAAAAAAg/wP+//+H/v//BwAAAAAAAAAAAAAAAAAAAAA=}]
chars-w1: #[bitset! 64#{AAAAAEJsAID+//+H/v//FwAAAAAAAAAAAAAAAAAAAAA=}]
chars-w*: #[bitset! 64#{AAAAAEJs/4P+//+H/v//FwAAAAAAAAAAAAAAAAAAAAA=}]
chars-p: #[bitset! 64#{AAAAAKJo/wP+//+H/v//BwAAAAAAAAAAAAAAAAAAAAA=}]
chars-sp: #[bitset! 64#{AAIAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=}]
chars-as: #[bitset! 64#{AAYAAP///////////////wAAAAAAAAAAAAAAAAAAAAA=}]
chars-up: #[bitset! 64#{AAAAAAAAAAAAAAAAAAAAAP////////////////////8=}]
; chars-ht: #[bitset! 64#{AAYAALv//6////////////////////////////////8=}]
chars-ht: #[bitset! 64#{AAYAALv//6///////////wAAAAAAAAAAAAAAAAAAAAA=}]
chars: #[bitset! 64#{//n///7///////////////////////////////////8=}]
nochar: #[bitset! 64#{AAYAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=}]
export [
chars-n chars-la chars-ua chars-a chars-an chars-hx
chars-w1 chars-w* chars-f chars-p chars-u chars-ud chars-sp
chars-up chars-as chars-ht chars nochar
]
]
;--## UTF-8
;-------------------------------------------------------------------##
context [
utf-2: #[bitset! 64#{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA=}]
utf-3: #[bitset! 64#{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAA=}]
utf-4: #[bitset! 64#{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wA=}]
utf-5: #[bitset! 64#{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8=}]
utf-b: #[bitset! 64#{AAAAAAAAAAAAAAAAAAAAAP//////////AAAAAAAAAAA=}]
utf-8: [utf-2 1 utf-b | utf-3 2 utf-b | utf-4 3 utf-b | utf-5 4 utf-b]
utf-os: [0 192 224 240 248 252]
utf-fc: [1 64 4096 262144 16777216]
get-ucs-code: func [char /local int][
int: 0
char: change char char/1 xor pick utf-os length? char
forskip char 1 [change char char/1 xor 128]
char: head reverse head char
forskip char 1 [int: (to-integer char/1) * (pick utf-fc index? char) + int]
all [int > 127 int <= 65535 int]
]
export [utf-8 get-ucs-code]
]
;--## STRING HELPERS
;-------------------------------------------------------------------##
context [
pad: func [text length [integer!] /with padding [char!]][
padding: any [padding #"0"]
text: form text
skip tail insert/dup text padding length negate length
]
url-encode: func [text [any-string!]][
parse/all copy text [
copy text any [
some chars-ud |
#" " text: (change back text #"+") |
skip text: (change/part back text join "%" enbase/base to-string text/-1 16 1)
]
]
text
]
deplus: func [text][
parse/all text [some [to #"+" text: (text: change text #" ") :text] to end]
head text
]
decrlf: func [text][
parse/all text [some [to crlf text: (text: change/part text #"^/" 2) :text] to end]
head text
]
url-decode: func [text [any-string!]][decrlf dehex deplus to-string text]
decode-query: func [query [string! none!] /local result name value][
query: any [query ""]
result: copy []
if query [
remove-each value query: parse/all query "&" [empty? value]
map/only query func [value][parse/all value "="]
foreach value query [change/only value parse/all value/1 "."]
]
parse query [
any [
into [
set name block! set value opt string! (
add-to result name any [all [value url-decode value] ""]
)
]
]
]
result
]
; doesn't work right, yet
decode-options: func [options [string! none!] type [datatype!]][
options: any [options ""]
options: parse lowercase options ";,"
map options func [val [string!] /local weight][
either parse/all val ["q=" weight: "0." integer!][
load weight
][
as :type val
]
]
]
compose-tags: func [body [string!] callback [any-function!] /local out tag block][
out: make string! length? body
while [tag: find body "=["][
insert/part tail out body offset? body tag
body: either error? err: try [
block: load/next tag: next tag
][
append out "**Tag Loading Error: #"
tag
][
append out any [callback first block ""]
second block
]
]
append out body
]
interpolate: func [body [string!] escapes [any-block!] /local out][
body: out: copy body
parse/all body [
any [
to #"%" body: (
body: change/part body reduce any [
select/case escapes body/2 body/2
] 2
) :body
]
]
out
]
replace: func [target [any-string!] search [char! any-string!] replace /all /case /local len rule][
len: either char? search [1][length? search]
rule: [any [to search target: (target: change/part target replace len) :target] to end]
rule/1: pick [opt any] none? all
do pick [parse parse/case] none? case target rule
head target
]
sanitize: func [text [any-string!] /local char][
parse/all copy text [
copy text any [
text: some chars-ht
| #"&" (text: change/part text "&" 1) :text
| #"<" (text: change/part text "<" 1) :text
| #">" (text: change/part text ">" 1) :text
| #"^"" (text: change/part text """ 1) :text
| #"^M" (remove text) :text
| copy char utf-8 (text: change/part text rejoin ["" get-ucs-code char ";"] length? char)
| skip (text: change/part text rejoin ["#(" to-integer text/1 ")"] 1) :text
; | skip (text: change text "#") :text
]
]
any [text ""]
]
; sanitize: func [text][
; replace/all text: copy text #"&" "&"
; replace/all text #"<" "<"
; replace/all text #">" ">"
; replace/all text #"^"" """
; ]
load-multipart: func [
[catch] data [binary!] boundary
/local store name content filetype filename
file-prototype qchars nchars dchars
][
store: copy []
file-prototype: context [name: data: type: meta: #[none]]
qchars: #[bitset! 64#{//////v///////////////////////////////////8=}]
nchars: #[bitset! 64#{//////////f///////////////////////////////8=}]
unless parse/all/case data [boundary data: to end][
raise "Postdata not Multipart"
]
boundary: join crlf boundary
unless parse/all/case data [
some [
"--" crlf end |
(name: content: filemime: filetype: filename: none)
crlf {Content-Disposition: form-data; name=}
[{"} copy name some qchars {"} | copy name some nchars]
(name: parse/all name ".")
opt [
{; filename=} [
[{"} copy filename any qchars {"} | copy filename any nchars]
crlf {Content-Type: } copy filetype to crlf
]
]
crlf crlf copy content to boundary boundary (
content: any [content ""]
either all [filetype filetype: as path! filetype][
filename: either filename [
as file! any [
find/last/tail filename #"/"
find/last/tail filename #"\"
find/last/tail filename #":"
filename
]
][%file.dat]
content: either filetype/1 = 'text [decrlf content][to-binary content]
add-to store name make file-prototype [
name: :filename type: :filetype data: :content
]
][
add-to store name to-string decrlf content
]
)
]
][
raise "Invalid Multipart Postdata"
]
store
]
export [
pad url-encode url-decode decode-query decode-options
load-multipart compose-tags interpolate replace sanitize
]
]
;--## PORT HELPERS
;-------------------------------------------------------------------##
context [
add-protocol: func ['name id handler /with block][
unless in system/schemes name [
system/schemes: make system/schemes compose [
(to-set-word name) #[none]
]
]
set in system/schemes name make system/standard/port compose [
scheme: name
port-id: (id)
handler: (handler)
passive: #[none]
cache-size: 5
proxy: make object! [host: port-id: user: pass: type: bypass: #[none]]
(block)
]
]
to-header: func [object [object!] /local header][
header: make string! (20 * length? first object)
foreach word next first object [
if get word: in object word [
insert tail header reduce [word ": " get word newline]
]
]
header
]
codes: [read 1 write 2 append 4 new 8 binary 32 lines 64 direct 524288]
get-port-flags: func [port words][
remove-each word copy words [
word: select codes word
word <> (port/state/flags and word)
]
]
chars: ; charset [#"a" - #"z" #"A" - #"Z" #"0" - #"9" "-_!+%"]
#[bitset! 64#{AAAAACIo/wP+//+H/v//BwAAAAAAAAAAAAAAAAAAAAA=}]
space!: context [
root: domain: path: target: folder: file: suffix: #[none]
]
get-space: func [base [url!] location [url!] /local space][
base: form base
space: make space! [uri: :location]
if all with/only space [
parse/all uri [
base
copy domain some chars #"/"
copy path any [some [some chars | #"."] #"/"]
copy target opt [any chars #"." 1 10 chars]
]
root: select config/spaces domain
] with/only space [
path: all [path to-file path]
target: all [target to-file target]
folder: join root any [path ""]
file: join folder any [target ""]
suffix: suffix? file
self
]
]
export [add-protocol to-header get-port-flags get-space]
]
;--## VALUES HELPERS
;-------------------------------------------------------------------##
context [
pad-zone: func [time /flat][
rejoin [
pick "-+" time/hour < 0
pad abs time/hour 2
either flat [""][#":"]
pad time/minute 2
]
]
get-iso-year: func [year [integer!] /local d1 d2][
d1: to-date join "4-1-" year
d2: to-date join "28-12-" year
return reduce [d1 + 1 - d1/weekday d2 + 7 - d2/weekday]
]
to-iso-week: func [date [date!] /local out d1 d2][
out: 0x0
set [d1 d2] get-iso-year out/y: date/year
case [
date < d1 [d1: first get-iso-year out/y: date/year - 1]
date > d2 [d1: first get-iso-year out/y: date/year + 1]
]
out/x: date + 8 - date/weekday - d1 / 7
out
]
date-codes: [
#"a" [copy/part pick system/locale/days date/weekday 3]
#"A" [pick system/locale/days date/weekday]
#"b" [copy/part pick system/locale/months date/month 3]
#"B" [pick system/locale/months date/month]
#"C" [to-integer date/year / 100]
#"d" [pad date/day 2]
#"D" [date/year #"/" pad date/month 2 #"/" pad date/day 2]
#"e" [date/day]
#"g" [pad (second to-iso-week date) // 100 2]
#"G" [second to-iso-week date]
#"h" [time/hour + 11 // 12 + 1]
#"H" [pad time/hour 2]
#"i" [any [get-class [st 1 21 31 nd 2 22 rd 3 23] date/day "th"]]
#"I" [pad time/hour + 11 // 12 + 1 2]
#"j" [pad date/julian 3]
#"J" [date/julian]
#"m" [pad date/month 2]
#"M" [pad time/minute 2]
#"p" [pick ["am" "pm"] time/hour < 12]
#"P" [pick ["AM" "PM"] time/hour < 12]
#"S" [pad round time/second 2]
#"t" [#"^-"]
#"T" [pad time/hour 2 #":" pad time/minute 2 #":" pad round time/second 2]
#"u" [date/weekday]
#"U" [pad to-integer date/julian + 6 - (date/weekday // 7) / 7 2]
#"V" [pad first to-iso-week date 2]
#"w" [date/weekday // 7]
#"W" [pad to-integer date/julian + 7 - date/weekday / 7 2]
#"y" [pad date/year // 100 2]
#"Y" [date/year]
#"z" [pad-zone/flat zone]
#"Z" [pad-zone zone]
]
form-date: func [date [date!] format [any-string!] /gmt /local time zone nyd][
all [
date/time date/zone
date/time: date/time - date/zone
date/time: date/time + date/zone: either gmt [0:00][config/zone]
]
time: any [date/time 0:00]
zone: any [date/zone config/zone 0:00]
interpolate format bind date-codes 'date
]
color-codes: [
#"r" [color/1] #"1" [to-char color/1]
#"g" [color/2] #"2" [to-char color/2]
#"b" [color/3] #"3" [to-char color/3]
#"a" [color/4] #"4" [to-char color/4]
#"R" [skip tail to-hex color/1 -2]
#"G" [skip tail to-hex color/2 -2]
#"B" [skip tail to-hex color/3 -2]
#"A" [skip tail to-hex color/4 -2]
]
form-color: func [color [tuple!] format [any-string!]][
bind color-codes 'color
color: 0.0.0.0 + color
interpolate format color-codes
]
pluralize: func [string [string!] count [number!]][
unless any [count = 1 count = -1][string: join string "s"]
reform [count string]
]
export [form-date to-local-time form-color pluralize]
]
;--## VALUES FILTER
;-------------------------------------------------------------------##
context [
id: [chars-la 0 15 chars-f]
word: [chars-w1 0 25 chars-w*]
number: [integer!]
integer: [opt #"-" number]
masks: reduce [
issue! [some chars-u]
logic! ["true" | "on" | "yes" | "1"]
word! [word]
url! [id #":" some [chars-u | #":" | #"/"]]
email! [some chars-u #"@" some chars-u]
path! [word 1 5 [#"/" [word | integer]]]
integer! [integer]
string! [some [some chars-as | utf-8]]
'positive [number]
'id [id]
'key [word 0 6 [#"." word]]
]
as: func [
[catch] type [datatype!] value [any-type!]
/where format [none! block! any-word!]
][
case/all [
none? format [format: select masks type]
none? format [if type = type? value [return value]]
any-word? format [format: select masks to-word format]
block? format [
unless parse/all form value format [return none]
]
type = path! [return load value]
]
attempt [make type value]
]
export [as]
]
;--## IMPORT
;-------------------------------------------------------------------##
context [
result: errors: #[none]
messages: [
not-included "is not included in the list"
excluded "is reserved"
invalid "is missing or invalid"
not-confirmed "doesn't match confirmation"
not-accepted "must be accepted"
empty "can't be empty"
blank "can't be blank"
too-long "is too long (maximum is %d characters)"
too-short "is too short (minimum is %d characters)"
wrong-length "is the wrong length (should be %d characters)"
not-a-number "is not a number"
too-many "has too many arguments"
]
datatype: [
'any-string! | 'binary! | 'block! | 'char! | 'date! | 'decimal! | 'email! | 'file! |
'get-word! | 'integer! | 'issue! | 'lit-path! | 'lit-word! | 'logic! | 'money! |
'none! | 'number! | 'pair! | 'paren! | 'path! | 'range! | 'refinement! |
'set-path! | 'set-word! | 'string! | 'tag! | 'time! | 'tuple! | 'url! | 'word!
]
else: #[none]
otherwise: [
['else | 'or][
set else string! | copy else any [word! string!]
] | (else: #[none])
]
source: key: value: target: type: format: constraints: else: none
constraint: use [is is-not? is-or-length-is op val val-type range group][
op: val: val-type: none
is: ['is | 'are]
is-or-length-is: [
[
['length | 'size] (val: length? value val-type: integer!)
| (val: :value val-type: :type)
] is
]
is-not?: ['not (op: false) | (op: true)]
[
is [
'accepted otherwise (
unless true = value [report not-accepted]
) |
'confirmed opt 'by set val get-word! otherwise (
val: to-word val
unless value = as/where :type get-from source :val format [
report not-confirmed
]
) |
is-not? 'within set group any-block! otherwise (
either found? find group value [
unless op [report excluded]
][
if op [report not-included]
]
)
] |
is-or-length-is [
is-not? 'between [set range [range! | into [2 val-type]]] otherwise (
either op [
case [
val < target: range/1 [report too-short]
val > target: range/2 [report too-long]
]
][
unless any [
val < range/1
val > range/2
][report excluded]
]
) |
'more-than set target val-type otherwise (
unless val > target [report too-short]
) |
'less-than set target val-type otherwise (
unless val < target [report too-long]
) |
set target val-type otherwise (
unless val = target [report wrong-length]
)
]
]
]
do-constraints: does [constraints: [any constraint]]
no-constraints: does [constraints: [to set-word! | to end]]
humanize: func [word][uppercase/part replace/all form word "-" " " 1]
report: func ['message [word!]][
message: any [
all [string? else else]
all [block? else select else message]
reform [humanize key any [select messages message ""]]
]
unless select errors :key [repend errors [:key copy []]]
append select errors :key interpolate message [
#"w" [form key]
#"W" [humanize key]
#"d" [form target]
#"t" [form type]
]
]
import: func [
[catch] source* [block! none!] spec [block!]
/report-to errs [block!]
/block /again /local required present
][
unless source* [return none]
errors: copy []
result: copy []
source: source* []
unless parse compose/deep/only spec [
any [
set key [set-word! | refinement!] (key: to-word key)
set required opt 'opt (required: required <> 'opt)
set type datatype (type: get type)
set format opt [block! | get-word!]
otherwise
(
value: either block [
pick source 1
][
get-from source :key
]
present: not any [
none? value
empty? trim/head/tail form value
]
either all [
present
value: either :type = block! [
either block? format [
import value format
][value]
][
as/where :type value format
]
][
if block [source: next source]
do-constraints
repend result [key value]
][
no-constraints
case [
all [present not block] [report invalid]
required [report blank]
not required [repend result [key none]]
]
]
)
constraints
]
][raise "Could not parse Import specification"]
all [block? errs insert clear errs errors]
unless qm/errors: all [not empty? errors errors][result]
]
get-one: func [data type /local res][
parse data [some [res: type to end break | skip]]
unless tail? res [take res]
]
get-some: func [data type /local pos res][
res: make block! length? data
parse data [some [pos: type (append/only res take pos) :pos | skip]]
unless empty? res [res]
]
match: func [
[catch] source* [block!] spec [block!]
/report-to errs [block!]
/local required type
][
source: copy :source*
errors: copy []
result: context append remove-each item copy spec [not set-word? item] none
unless parse spec [
some [
set key set-word! (key: to-word key)
set required ['opt | 'any | 'some | none]
copy type [datatype any ['| datatype]]
otherwise
(
switch/default required [
any [
value: get-some source type
either value [do-constraints][no-constraints]
]
opt [
value: get-one source type
either value [do-constraints][no-constraints]
]
some [
value: get-some source type
either value [do-constraints][no-constraints report invalid]
]
][
value: get-one source type
either value [do-constraints][no-constraints report invalid]
]
result/(key): value
)
constraints
]
][raise "Could not parse Match specification"]
all [block? errs insert clear errs errors]
unless empty? source [key: 'match report too-many]
unless qm/errors: all [not empty? errors errors][result]
]
export [import match]
]
;--## Q(uick)TAG DIALECT
;-------------------------------------------------------------------##
context [
qtags: []
form-val: func [val /local value][
val: switch/default type?/word val [
get-word! [
if value: get/any :val [
rejoin [{ } val {="} sanitize form value {"}]
]
]
word! [
all [val: get val sanitize form val]
]
none! [val]
string! [val]
][sanitize form val]
any [val ""]
]
add-qtag: func ['name [word!] format [block!] spec [block!] prep [block!]][
repend qtags [
name
func [[catch] spec [block!]] compose/deep/only [
throw-on-error [
either spec: match spec (spec) [
spec: make spec (prep)
rejoin map/copy with/only spec (format) :form-val
][
raise (rejoin ["!!!Invalid " uppercase form name " tag."])
]
]
]
]
]
add-qtag a [
""
][
href: file! | url! | path!
id: opt issue!
class: any refinement!
title: opt string!
accesskey: opt char!
rel: any lit-word!
][
all [path? href href: link-to :href]
]
add-qtag div [""
][
src: file! | url! | path!
size: opt pair!
alt: string!
title: opt string!
id: opt issue!
class: any refinement!
][
all [path? src src: link-to :src]
size: any [size -1x-1]
set [width height] reduce [size/x size/y]
all [width < 0 width: none] all [height < 0 height: none]
]
add-qtag form [
"
** <%= reason-type %>: <%= reason-message %>
** Where: <%= reason-where %>
** Near: <%= sanitize mold reason/near %>
[ insert find body rejoin [ {^/
}
remove sanitize log
{^/} ] ][ repend body ["^/" log] ] ] ; find request/accept-encodings 'deflate [ ; set-header 'Content-Encoding "deflate" ; ; IE and Safari need deflate header and footer removed ; clear skip tail remove remove body: compress body -8 ; ] ] unless find status-codes status [status: 500] status: reform ["Status:" status select status-codes status] set-header 'Content-Type either find [text application] type/1 [ rejoin [form type "; charset=" charset] ][form type] set-header 'Content-Length length? body insert body reduce [status newline headers newline] send-response body ] ] export [publish probe] ] ;--## CONTROLLER ;-------------------------------------------------------------------## context [ root: qm://system/controllers/ rendered?: #[false] redirect-to: func [[catch] 'url [file! url! path! none!] /back /status response-code [integer!]][ if rendered? [raise "Already Rendered!"] qm/response/status: any [response-code 302] qm/response/template: #[none] case [ all [back back: as url! get-header "HTTP_REFERER"][url: :back] none? url [raise "Redirect requires a valid URL"] all [file? url not #"/" = first url][insert url #"/"] path? url [url: link-to :url] ] qm/response/set-header 'Location url rendered?: #[true] ] render: func [ [catch] body [file! string! url! binary! none!] /status code [integer!] /as 'type [path!] /template master [file! string! url! binary! none!] /partial /charset encoding /local path ][ if rendered? [raise "Already Rendered!"] qm/response/body: :body case/all [ status [qm/response/status: :code] as [qm/response/type: :type] file? body [ qm/response/body: clean-view-path body partial ] ] qm/response/template: case [ binary? body [none] all [partial not template][none] not template [qm/response/template] file? master [clean-view-path master false] string? master [master] ] rendered?: #[true] ] print: func [value][render/as/template reform value text/plain none] load-controller: use [actions event action filter current][ actions: [] action: use [name class options][ class: context options: [spec: code: none] with/only class [ (with class options) 'action set name string! set spec opt block! 'does set code block! (repend actions ['action name make class []]) ] ] event: use [name code][ [ 'event set name string! 'does set code block! (repend actions ['event name code]) ] ] filter: use [names test code][ [ 'protect copy names some string! set test paren! set code block! (repend actions ['filter names reduce [test code]]) ] ] func [[catch] name /local file meta][ actions: copy [] unless all [ (all [name name <> ""]) (exists? file: join root [name %.r]) (block? file: load/header file) ('controller = get in meta: pop file 'type) ][ return none ] insert actions meta unless parse/all file [some [action | event | filter]][ name: rejoin ["Invalid Controller Spec: %controllers/" name ".r"] raise :name ] actions ] ] route: func [request [object!] /local actions file default args code] with/only qm [ controller: any [ pop request/path-info config/default-controller ] if all [ controller controller/1 = #"_" ][ render "Invalid Controller" exit ] unless actions: load-controller controller [ ; render "No Controller" render/status %errors/notfound.rsp 404 exit ] response/template: get in actions/1 'template use [default][ request/action: pick request/path-info 1 default: get in actions/1 'default case [ find actions reduce ['action request/action][ remove request/path-info action: request/action ] find actions reduce ['action default][ action: default ] else [ raise "No Action" ; render/status %errors/notfound.rsp 404 exit ] ] ] use [startup][ all [ startup: load-controller "_startup" insert tail actions next startup ] ] foreach [type scope details] next actions with/only events: context [ start: prepare: filter: this: none ][ case [ type = 'event [ switch scope [ "web-start" [start: :details] "prepare" [prepare: any [prepare details]] ] ] all [ type = 'filter find scope action ][ filter: append any [filter []] details ] scope = action [ this: any [this details] ] ] ] unless events/this [ response/template: none render/status %errors/notfound.rsp 404 exit ] unless args: import/block request/path-info any [events/this/spec []][ response/template: none render/status %errors/badrequest.rsp 400 ; print "Bad Request" exit exit ] view-path: dirize to-file controller qm: make qm context with/only qm with/only 'route compose/deep [ (third models) header: (first actions) title: (rejoin [uppercase/part form controller 1 " :: " uppercase/part form action 1]) (events/start) (events/prepare) (map-each [word value] args [word: to-set-word word]) case [ (any [events/filter []]) else [(events/this/code)] ] ] qm/binding: in qm 'self either rendered? [#[true]][ if action [ render head insert %.rsp action ] ] ] export [route] ] ;--## ENGAGE ;-------------------------------------------------------------------## try-else [ with qm [ engage-model route request disengage-model publish response ] ][ reason: make disarm reason [] qm/binding: 'reason qm/handler: %.rsp reason-type: sanitize system/error/(reason/type)/type reason-message: sanitize reform bind envelope system/error/(reason/type)/(reason/id) reason reason-where: sanitize mold reason/where with qm/response [ status: 500 type: 'text/html template: #[none] body: trim/head trim/with {