summaryrefslogtreecommitdiffstats
path: root/tcllib/modules/rest
diff options
context:
space:
mode:
authorWilliam Joye <wjoye@cfa.harvard.edu>2016-10-27 19:39:39 (GMT)
committerWilliam Joye <wjoye@cfa.harvard.edu>2016-10-27 19:39:39 (GMT)
commitea28451286d3ea4a772fa174483f9a7a66bb1ab3 (patch)
tree6ee9d8a7848333a7ceeee3b13d492e40225f8b86 /tcllib/modules/rest
parentb5ca09bae0d6a1edce939eea03594dd56383f2c8 (diff)
parent7c621da28f07e449ad90c387344f07a453927569 (diff)
downloadblt-ea28451286d3ea4a772fa174483f9a7a66bb1ab3.zip
blt-ea28451286d3ea4a772fa174483f9a7a66bb1ab3.tar.gz
blt-ea28451286d3ea4a772fa174483f9a7a66bb1ab3.tar.bz2
Merge commit '7c621da28f07e449ad90c387344f07a453927569' as 'tcllib'
Diffstat (limited to 'tcllib/modules/rest')
-rw-r--r--tcllib/modules/rest/ChangeLog75
-rw-r--r--tcllib/modules/rest/bitly34
-rw-r--r--tcllib/modules/rest/couchdb56
-rw-r--r--tcllib/modules/rest/delicious131
-rw-r--r--tcllib/modules/rest/facebook93
-rw-r--r--tcllib/modules/rest/flickr292
-rw-r--r--tcllib/modules/rest/gcal102
-rw-r--r--tcllib/modules/rest/gdocs87
-rw-r--r--tcllib/modules/rest/pkgIndex.tcl2
-rw-r--r--tcllib/modules/rest/rest.man538
-rw-r--r--tcllib/modules/rest/rest.tcl829
-rw-r--r--tcllib/modules/rest/twitter69
-rw-r--r--tcllib/modules/rest/yboss36
-rw-r--r--tcllib/modules/rest/yweather19
14 files changed, 2363 insertions, 0 deletions
diff --git a/tcllib/modules/rest/ChangeLog b/tcllib/modules/rest/ChangeLog
new file mode 100644
index 0000000..df29849
--- /dev/null
+++ b/tcllib/modules/rest/ChangeLog
@@ -0,0 +1,75 @@
+2013-05-30 Andreas Kupries <andreask@activestate.com>
+
+ * rest.tcl (::rest::_call): [Bug 3613726][Allura 1370]: Added
+ status code 303 ("See Other") to the set of codes returning with
+ a location error instead of a plain one.
+
+2013-02-01 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.15 ========================
+ *
+
+2011-12-13 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.14 ========================
+ *
+
+2011-01-24 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.13 ========================
+ *
+
+2009-12-07 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.12 ========================
+ *
+
+2009-10-14 Aaron Faupell <afaupell@users.sourceforge.net>
+
+ * rest.tcl: add -- to denote end of options
+ add body types "arg" and "mime_multipart"
+ fix typo bug in option parsing
+ make %variable substitution occur for content-type
+ make content-type in header config override http module
+ fix problem with unset option
+ dont append ? to urls unless there are parameters following
+ add basic google docs api
+
+2009-03-23 Andreas Kupries <andreask@activestate.com>
+
+ * rest.man: Fixed all the reported syntax errors. Quick hack,
+ large sections of the document were unformatted and not touched,
+ I did not have the time to perform big cleanup.
+ * rest.tcl: Fixed syntax error in parse_opts.
+
+2009-03-21 Aaron Faupell <afaupell@users.sourceforge.net>
+ rest.tcl: comment out debugging code which dumped the proc contents
+ add rest::simple command for making simple requests
+ change _format procs for format for easier use with simple calls
+ add version to http package require
+ add format as an alias to result config option
+ if a transform is a single cmd it is called directly instead of creating a proc
+
+2009-03-11 Aaron Faupell <afaupell@users.sourceforge.net>
+ rest.tcl: make http methods upper case
+ add 301 to redirect errors
+
+2009-02-06 Aaron Faupell <afaupell@users.sourceforge.net>
+
+ twitter: forgot to check this one in
+ flickr: fixed some broken or incomplete calls
+
+2009-02-05 Aaron Faupell <afaupell@users.sourceforge.net>
+
+ rest.tcl: fix some errors regarding static args, the body command
+ rest.man: add more documentation and examples. still needs formatting
+ facebook: remove extraneous line in sign proc
+
+2009-02-04 Aaron Faupell <afaupell@users.sourceforge.net>
+
+ Initial commit
+
diff --git a/tcllib/modules/rest/bitly b/tcllib/modules/rest/bitly
new file mode 100644
index 0000000..2d21305
--- /dev/null
+++ b/tcllib/modules/rest/bitly
@@ -0,0 +1,34 @@
+# documentation: http://code.google.com/p/bitly-api/wiki/ApiDocumentation
+
+package require rest
+
+set bitly(shorten) {
+ url http://api.bit.ly/v3/shorten
+ req_args { login: apiKey: }
+ opt_args { domain: }
+ body { argument longUrl }
+ check_result { {[dict get $result status_code] == "200"} {} }
+}
+
+set bitly(expand) {
+ url http://api.bit.ly/v3/expand
+ req_args { login: apiKey: }
+ body { arg shortUrl }
+ check_result { {[dict get $result status_code] == "200"} {} }
+}
+
+set bitly(clicks) {
+ url http://api.bit.ly/v3/clicks
+ req_args { login: apiKey: }
+ body { arg shortUrl }
+ check_result { {[dict get $result status_code] == "200"} {} }
+}
+
+set bitly(referrers) {
+ url http://api.bit.ly/v3/referrers
+ req_args { login: apiKey: }
+ body { arg shortUrl }
+ check_result { {[dict get $result status_code] == "200"} {} }
+}
+
+rest::create_interface bitly
diff --git a/tcllib/modules/rest/couchdb b/tcllib/modules/rest/couchdb
new file mode 100644
index 0000000..a59e18b
--- /dev/null
+++ b/tcllib/modules/rest/couchdb
@@ -0,0 +1,56 @@
+# documentation: http://wiki.apache.org/couchdb/HTTP_Document_API
+
+package require rest
+
+set couchdb(list_dbs) {
+ url http://%server%/_all_dbs/
+ result json
+}
+
+set couchdb(create_db) {
+ url http://%server%/%name%/
+ method put
+}
+
+set couchdb(delete_db) {
+ url http://%server%/%name%/
+ method delete
+}
+
+set couchdb(db_info) {
+ url http://%server%/%name%/
+}
+
+set couchdb(list_docs) {
+ url http://%server%/%db%/_all_docs
+ opt_args { descending: startkey: endkey: limit: }
+}
+
+set couchdb(get) {
+ url http://%server%/%db%/%doc%
+}
+
+set couchdb(put) {
+ url http://%server%/%db%/%doc%
+ method put
+}
+
+set couchdb(delete) {
+ url http://%server%/%db%/%doc%
+ req_args { rev: }
+ method delete
+}
+
+set couchdb(copy) {
+ url http://%server%/%db%/%from%
+ method copy
+ headers { Destination: %to% }
+}
+
+set couchdb(move) {
+ url http://%server%/%db%/%from%
+ method copy
+ headers { Destination: %to% }
+}
+
+rest::create_interface couchdb \ No newline at end of file
diff --git a/tcllib/modules/rest/delicious b/tcllib/modules/rest/delicious
new file mode 100644
index 0000000..97f97d1
--- /dev/null
+++ b/tcllib/modules/rest/delicious
@@ -0,0 +1,131 @@
+# documentation: http://delicious.com/help/api
+
+package require rest
+
+set delicious(updated) {
+ url https://api.del.icio.us/v1/posts/update
+ auth basic
+ result raw
+ post_transform {
+ regexp {<update time=\"(.*?)\"} $result -> update
+ return [clock scan [string map {T " " Z " UTC"} $update]]
+ }
+}
+
+set delicious(add_post) {
+ url https://api.del.icio.us/v1/posts/add
+ auth basic
+ req_args { url: description: }
+ opt_args { extended: tags: dt: replace: shared: }
+ check_result { {[regexp {<result code=\"done} $result]} {} }
+}
+
+set delicious(delete_post) {
+ url https://api.del.icio.us/v1/posts/delete
+ auth basic
+ req_args { url: }
+ check_result { {[regexp {<result code=\"done} $result]} {} }
+}
+
+set delicious(get_posts) {
+ url https://api.del.icio.us/v1/posts/get
+ auth basic
+ opt_args { url: tag: dt: hashes: meta: }
+}
+
+set delicious(recent_posts) {
+ url https://api.del.icio.us/v1/posts/recent
+ auth basic
+ opt_args { tag: }
+}
+
+set delicious(post_dates) {
+ url https://api.del.icio.us/v1/posts/dates
+ auth basic
+ opt_args { tag: count: }
+}
+
+set delicious(get_all_posts) {
+ url https://api.del.icio.us/v1/posts/all
+ auth basic
+ opt_args { tag: start: results: fromdt: todt: meta: }
+}
+
+set delicious(get_hashes) {
+ url https://api.del.icio.us/v1/posts/all
+ auth basic
+ static_args { hashes {} }
+}
+
+set delicious(get_tags) {
+ url https://api.del.icio.us/v1/tags/get
+ auth basic
+}
+
+set delicious(delete_tag) {
+ url https://api.del.icio.us/v1/tags/delete
+ auth basic
+ req_args { tag: }
+ check_result { {[regexp {<result>done} $result]} {} }
+}
+
+set delicious(rename_tag) {
+ url https://api.del.icio.us/v1/tags/rename
+ auth basic
+ req_args { old: new: }
+ check_result { {[regexp {<result>done} $result]} {} }
+}
+
+set delicious(get_bundles) {
+ url https://api.del.icio.us/v1/bundles/all
+ auth basic
+ opt_args { bundle: }
+}
+
+set delicious(set_bundle) {
+ url https://api.del.icio.us/v1/bundles/set
+ auth basic
+ req_args { bundle: tags: }
+ check_result { {[regexp {<result>ok} $result]} {} }
+}
+
+set delicious(delete_bundle) {
+ url https://api.del.icio.us/v1/bundles/delete
+ auth basic
+ req_args { bundle: }
+ check_result { {[regexp {<result>done} $result]} {} }
+}
+
+set delicious(public_posts) {
+ url http://feeds.delicious.com/v2/json/%user%/%tags:%
+ opt_args { count: }
+}
+
+set delicious(modify_post) {
+ url https://api.del.icio.us/v1/posts/add
+ auth basic
+ req_args { post: }
+ opt_args { description: extended: tags: dt: shared: }
+ check_result { {[regexp {<result code=\"done} $result]} {} }
+ result raw
+ input_transform {
+ set new [dict remove [dict get $query post] hash others meta]
+ foreach {from to} {href url tag tags time dt} {
+ set v [dict get $new $from]
+ set new [dict remove $new $from]
+ dict set new $to $v
+ }
+ dict for {k v} [dict remove $query post] {
+ if {$v == ""} {
+ set new [dict remove $new $k]
+ continue
+ }
+ if {$k == "dt"} { set v [string trimright $v Z] }
+
+ set new [dict replace $new $k $v]
+ }
+ return $new
+ }
+}
+
+rest::create_interface delicious
diff --git a/tcllib/modules/rest/facebook b/tcllib/modules/rest/facebook
new file mode 100644
index 0000000..9ce79b4
--- /dev/null
+++ b/tcllib/modules/rest/facebook
@@ -0,0 +1,93 @@
+# documentation: http://wiki.developers.facebook.com/index.php/Category:API_functions
+
+package require rest
+package require tls
+::http::register https 443 [list ::tls::socket]
+package require md5
+
+set facebook(auth.createToken) {
+ description {Creates an auth_token to be passed in as a parameter to
+ loginLink and then to auth.getSession after the user has
+ logged in. The user must log in soon after you create this
+ token. }
+ url http://api.facebook.com/restserver.php
+ method post
+ auth { sign sign }
+ req_args { api_key: secret: }
+ static_args { v 1.0 format json method Auth.createToken }
+ check_result { {} {[string match "\{error_code*" $result]} }
+ post_transform { return [string trim $result \"] }
+}
+
+set facebook(auth.getSession) {
+ url https://api.facebook.com/restserver.php
+ method post
+ auth { sign sign }
+ req_args { api_key: auth_token: secret: }
+ static_args { v 1.0 format json method Auth.getSession }
+ check_result { {} {[string match "\{error_code*" $result]} }
+}
+
+set facebook(friends.get) {
+ url http://api.facebook.com/restserver.php
+ auth { sign sign }
+ req_args { api_key: secret: call_id: }
+ opt_args { session_key: flid: uid: }
+ static_args { v 1.0 format json method Friends.get }
+ post_transform { return [split [string trim $result \[\]] ,] }
+ check_result { {} {[string match "\{error_code*" $result]} }
+}
+
+set facebook(users.getInfo) {
+ url http://api.facebook.com/restserver.php
+ auth { sign sign }
+ req_args { api_key: secret: call_id: uids: fields: }
+ opt_args { session_key: }
+ static_args { v 1.0 format json Users.getInfo }
+ check_result { {} {[string match "\{error_code*" $result]} }
+}
+
+set facebook(users.setStatus) {
+ url http://api.facebook.com/restserver.php
+ auth { sign sign }
+ req_args { api_key: secret: call_id: }
+ opt_args { session_key: status: clear: status_includes_verb: uid: }
+ static_args { v 1.0 format json Users.setStatus }
+ check_result { {} {[string match "\{error_code*" $result]} }
+}
+
+set facebook(groups.get) {
+ url http://api.facebook.com/restserver.php
+ auth { sign sign }
+ req_args { api_key: secret: session_key: call_id: }
+ opt_args { gids: uid: }
+ static_args { v 1.0 format json method Groups.get }
+ check_result { {} {[string match "\{error_code*" $result]} }
+}
+
+set facebook(notifications.get) {
+ url http://api.facebook.com/restserver.php
+ auth { sign sign }
+ req_args { api_key: secret: session_key: call_id: }
+ static_args { v 1.0 format json method Notifications.get }
+ check_result { {} {[string match "\{error_code*" $result]} }
+}
+
+rest::create_interface facebook
+
+proc ::facebook::sign {query} {
+ set str ""
+ set secret [dict get $query secret]
+ set query [dict remove $query secret]
+ foreach x [lsort [dict keys $query]] {
+ append str $x=[dict get $query $x]
+ }
+ append str $secret
+ dict append query sig [string tolower [md5::md5 -hex $str]]
+ return $query
+}
+
+proc ::facebook::loginLink {args} {
+ set query [lindex [::rest::parse_opts {} {api_key: auth_token:} {} $args] 0]
+ return http://www.facebook.com/login.php?api_key=[dict get $query api_key]&v=1.0&auth_token=[dict get $query auth_token]
+}
diff --git a/tcllib/modules/rest/flickr b/tcllib/modules/rest/flickr
new file mode 100644
index 0000000..d7b85c0
--- /dev/null
+++ b/tcllib/modules/rest/flickr
@@ -0,0 +1,292 @@
+# documentation: http://www.flickr.com/services/api/
+
+package require rest
+package require md5
+
+set flickr(_proto) {
+ url http://api.flickr.com/services/rest/
+ req_args { auth_token: api_key: secret: }
+ auth { sign sign }
+}
+
+set flickr(activity.userComments) {
+ copy _proto
+ opt_args { per_page: page: }
+ static_args { -method flickr.activity.userComments }
+}
+
+set flickr(activity.userPhotos) {
+ copy _proto
+ opt_args { per_page: page: timeframe: }
+ static_args { -method flickr.activity.userPhotos }
+}
+
+set flickr(auth.getFrob) {
+ url http://api.flickr.com/services/rest/
+ req_args { api_key: secret: }
+ static_args { -method flickr.auth.getFrob }
+ auth { sign sign }
+ check_result { {} {[regexp {stat fail} $result]} }
+ post_transform { return [lindex $result 2 0 2 0 1] }
+}
+
+set flickr(auth.getToken) {
+ url http://api.flickr.com/services/rest/
+ req_args { api_key: frob: secret: }
+ static_args { -method flickr.auth.getToken }
+ check_result { {} {[regexp {stat fail} $result]} }
+ auth { sign sign }
+}
+
+set flickr(blogs.getList) {
+ copy _proto
+ static_args { -method flickr.blogs.getList }
+}
+
+set flickr(contacts.getList) {
+ copy _proto
+ opt_args { per_page: page: filter: }
+ static_args { -method flickr.contats.getList }
+}
+
+set flickr(contacts.getListRecentlyUploaded) {
+ copy _proto
+ opt_args { date_lastupload: filter: }
+ static_args { -method flickr.contacts.getListRecentlyUploaded }
+}
+
+set flickr(contacts.getPublicList) {
+ copy _proto
+ req_args { api_key: user_id: }
+ opt_args { per_page: page: }
+ static_args { -method flickr.contacts.getPublicList }
+}
+
+set flickr(favorites.add) {
+ copy _proto
+ body none
+ method post
+ req_args { auth_token: api_key: secret: photo_id: }
+ static_args { -method flickr.favorites.add }
+}
+
+set flickr(favorites.getList) {
+ copy _proto
+ opt_args { user_id: min_fav_date: max_fav_date: extras: per_page: page: }
+ static_args { -method flickr.favorites.getList }
+}
+
+set flickr(favorites.getPublicList) {
+ copy _proto
+ req_args { api_key: user_id: }
+ opt_args { min_fav_date: max_fav_date: extras: per_page: page: }
+ static_args { -method flickr.favorites.getPublicList }
+}
+
+set flickr(favorites.remove) {
+ copy _proto
+ body none
+ method post
+ req_args { auth_token: api_key: secret: photo_id: }
+ static_args { -method flickr.favorites.remove }
+}
+
+set flickr(groups.browse) {
+ copy _proto
+ opts_args { cat_id: }
+ static_args { -method flickr.groups.browse }
+}
+
+set flickr(groups.getInfo) {
+ copy _proto
+ req_args { api_key: group_id: }
+ opt_args { lang: }
+ static_args { -method flickr.groups.getInfo }
+}
+
+set flickr(groups.search) {
+ copy _proto
+ req_args { api_key: text: }
+ opt_args { per_page: page: }
+ static_args { -method flickr.groups.search }
+}
+
+set flickr(interestingness.getList) {
+ copy _proto
+ req_args { api_key: }
+ opt_args { date: extras: per_page: page: }
+ static_args { -method flickr.interestingness.getList }
+}
+
+set flickr(people.findByEmail) {
+ copy _proto
+ req_args { api_key: find_email: }
+ static_args { -method flickr.people.findByEmail }
+}
+
+set flickr(people.findByUsername) {
+ copy _proto
+ req_args { api_key: username: }
+ static_args { -method flickr.people.findByUsername }
+}
+
+set flickr(people.getInfo) {
+ copy _proto
+ req_args { api_key: user_id: }
+ static_args { -method flickr.people.getInfo }
+}
+
+set flickr(people.getPublicGroups) {
+ copy _proto
+ req_args { api_key: user_id: }
+ static_args { -method flickr.people.getPublicGroups }
+}
+
+set flickr(people.getPublicPhotos) {
+ copy _proto
+ req_args { api_key: user_id: }
+ opt_args { safe_search: extras: per_page: page: }
+ static_args { -method flickr.people.getPublicPhotos }
+}
+
+set flickr(photos.addTags) {
+ copy _proto
+ body none
+ method post
+ req_args { auth_token: api_key: secret: photo_id: tags: }
+ static_args { -method flickr.photos.addTags }
+}
+
+set flickr(photos.delete) {
+ copy _proto
+ body none
+ method post
+ req_args { auth_token: api_key: secret: photo_id: }
+ static_args { -method flickr.photos.delete }
+}
+
+set flickr(photos.getContactsPhotos) {
+ copy _proto
+ opt_args { count: just_friends: single_photo: include_self: extras: }
+ static_args { -method flickr.photos.getContactsPhotos }
+}
+
+set flickr(photos.getCounts) {
+ copy _proto
+ opt_args { dates: taken_dates: }
+ static_args { -method flickr.photos.getCounts }
+}
+
+set flickr(photos.getExif) {
+ copy _proto
+ req_args { api_key: photo_id: }
+ opt_args { secret: }
+ static_args { -method flickr.photos.getExif }
+}
+
+set flickr(photos.getInfo) {
+ copy _proto
+ req_args { api_key: photo_id: }
+ opt_args { secret: }
+ static_args { -method flickr.photos.getInfo }
+}
+
+set flickr(photos.getRecent) {
+ copy _proto
+ req_args { api_key: }
+ opt_args { per_page: page: extras: }
+ static_args { -method flickr.photos.getRecent }
+}
+
+set flickr(photos.getSizes) {
+ copy _proto
+ req_args { api_key: photo_id: }
+ static_args { -method flickr.photos.getSizes }
+}
+
+set flickr(photos.search) {
+ copy _proto
+ req_args { api_key: }
+ opt_args { user_id: tags: tag_mode: text: min_upload_date: max_upload_date:
+ min_taken_date: max_taken_date: license: sort: privacy_filter:
+ extras: page: per_page: group_id: safe_search: }
+ static_args { -method flickr.photos.search }
+}
+
+set flickr(photos.setTags) {
+ copy _proto
+ body none
+ method post
+ req_args { auth_token: api_key: secret: photo_id: tags: }
+ static_args { -method flickr.photos.setTags }
+}
+
+set flickr(photos.comments.addComment) {
+ copy _proto
+ body none
+ method post
+ req_args { auth_token: api_key: secret: photo_id: comment_text: }
+ static_args { -method flickr.photos.comments.addComment }
+}
+
+set flickr(photos.comments.getList) {
+ copy _proto
+ req_args { api_key: photo_id: }
+ opt_args { min_comment_date: max_comment_date: }
+ static_args { -method flickr.photos.comments.getList }
+}
+
+set flickr(photosets.addPhoto) {
+ copy _proto
+ body none
+ method post
+ req_args { auth_token: api_key: secret: photo_id: photoset_id: }
+ static_args { -method flickr.photosets.addPhoto }
+}
+
+set flickr(photosets.create) {
+ copy _proto
+ body none
+ method post
+ req_args { auth_token: api_key: secret: title: primary_photo_id: }
+ opt_args { description: }
+ static_args { -method flickr.photosets.create }
+}
+
+set flickr(photosets.getInfo) {
+ copy _proto
+ req_args { api_key: photoset_id: }
+ static_args { -method flickr.photosets.getInfo }
+}
+
+set flickr(photosets.getPhotos) {
+ copy _proto
+ req_args { api_key: photoset_id: }
+ opt_args { per_page: page: media: extras: privacy_filter: }
+ static_args { -method flickr.photosets.getPhotos }
+}
+
+set flickr(photosets.getList) {
+ copy _proto
+ req_args { api_key: user_id: }
+ static_args { -method flickr.photosets.getList }
+}
+
+
+rest::create_interface flickr
+
+proc ::flickr::sign {query} {
+ set str [dict get $query secret]
+ set query [dict remove $query secret]
+ foreach x [lsort [dict keys $query]] {
+ append str $x[dict get $query $x]
+ }
+ dict append query api_sig [md5::md5 -hex $str]
+ return $query
+}
+
+proc ::flickr::loginLink {args} {
+ set query [lindex [::rest::parse_opts {} {api_key: secret: perms: frob:} {} $args] 0]
+ set query [::flickr::sign $query]
+ return http://flickr.com/services/auth/?[eval ::http::formatQuery $query]
+}
diff --git a/tcllib/modules/rest/gcal b/tcllib/modules/rest/gcal
new file mode 100644
index 0000000..e1eeab4
--- /dev/null
+++ b/tcllib/modules/rest/gcal
@@ -0,0 +1,102 @@
+# documentation: http://code.google.com/apis/calendar/docs/2.0/developers_guide_protocol.html
+
+package require rest
+package require tls
+::http::register https 443 [list ::tls::socket]
+
+set gcal(auth) {
+ url https://www.google.com/accounts/ClientLogin
+ method post
+ req_args { Email: Passwd: }
+ opt_args { source:tclrest }
+ static_args { service cl }
+ post_transform {
+ regexp {Auth=(.*)\n} $result -> result
+ return $result
+ }
+}
+
+set gcal(all_calendars) {
+ url http://www.google.com/calendar/feeds/default/allcalendars/%projection:full%
+ headers { Authorization {GoogleLogin auth=%token%} }
+ opt_args { gsessionid: }
+ body required
+}
+
+set gcal(own_calendars) {
+ url http://www.google.com/calendar/feeds/default/owncalendars/%projection:full%
+ headers { Authorization {GoogleLogin auth=%token%} }
+ opt_args { gsessionid: }
+}
+
+set gcal(new_calendar) {
+ url http://www.google.com/calendar/feeds/default/owncalendars/full
+ method post
+ content-type application/atom+xml
+ headers { Authorization {GoogleLogin auth=%token%} }
+ opt_args { gsessionid: }
+}
+
+set gcal(edit_calendar) {
+ url http://www.google.com/calendar/feeds/default/owncalendars/full/%calendar%
+ method put
+ content-type application/atom+xml
+ headers { Authorization {GoogleLogin auth=%token%} }
+ opt_args { gsessionid: }
+}
+
+set gcal(delete_calendar) {
+ url http://www.google.com/calendar/feeds/default/owncalendars/full/%calendar%
+ method delete
+ headers { Authorization {GoogleLogin auth=%token%} }
+ opt_args { gsessionid: }
+}
+
+set gcal(all_events) {
+ url http://www.google.com/calendar/feeds/%user:default%/%visibility:private%/%projection:full%
+ headers { Authorization {GoogleLogin auth=%token%} }
+ opt_args { gsessionid: }
+}
+
+set gcal(new_event) {
+ url http://www.google.com/calendar/feeds/default/private/full
+ method post
+ content-type application/atom+xml
+ headers { Authorization {GoogleLogin auth=%token%} }
+ opt_args { gsessionid: }
+}
+
+rest::create_interface gcal
+
+proc ::gcal::handle_redir {args} {
+ if {[catch {eval $args} out]} {
+ #puts "catch $out"
+ if {[lindex $out 1] == "302"} {
+ eval [linsert $args 1 -gsessionid [rest::parameters [lindex $out 2] gsessionid]]
+ } else {
+ return -code error $out
+ }
+ }
+}
+
+proc ::gcal::create_single_event_object {args} {
+ set defaults [dict create \
+ text "" \
+ status confirmed \
+ where "" \
+ ]
+ set args [lindex [::rest::parse_opts {} {title: start: end:} {text: status: where:} $args] 0]
+ set args [dict merge $defaults $args]
+
+ set event {}
+ lappend event "<entry xmlns='http://www.w3.org/2005/Atom' xmlns:gd='http://schemas.google.com/g/2005'>"
+ lappend event "<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event'></category>"
+ lappend event "<title type='text'>[dict get $args title]</title>"
+ lappend event "<content type='text'>[dict get $args text]</content>"
+ lappend event "<gd:when startTime='[clock format [clock scan [dict get $args start]] -format "%Y-%m-%dT%TZ"]' endTime='[clock format [clock scan [dict get $args end]] -format "%Y-%m-%dT%TZ"]'></gd:when>"
+ lappend event "<gd:eventStatus value='http://schemas.google.com/g/2005#event.[dict get $args status]'> </gd:eventStatus>"
+ lappend event "<gd:where valueString='[dict get $args where]'></gd:where>"
+ lappend event "</entry>"
+ return [join $event \n]
+}
+
diff --git a/tcllib/modules/rest/gdocs b/tcllib/modules/rest/gdocs
new file mode 100644
index 0000000..2a761c7
--- /dev/null
+++ b/tcllib/modules/rest/gdocs
@@ -0,0 +1,87 @@
+# documentation: http://code.google.com/apis/documents/docs/3.0/developers_guide_protocol.html
+
+package require rest
+
+package require tls
+::http::register https 443 [list ::tls::socket]
+
+set gdocs(auth) {
+ url https://www.google.com/accounts/ClientLogin
+ method post
+ req_args { Email: Passwd: }
+ opt_args { source:tclrest }
+ static_args { service writely }
+ post_transform {
+ regexp {Auth=(.*)\n} $result -> result
+ return $result
+ }
+}
+
+set gdocs(doclist) {
+ url http://docs.google.com/feeds/default/private/full
+ headers {
+ GData-Version 3.0
+ Authorization {GoogleLogin auth=%token%}
+ }
+ format tdom
+ post_transform {
+ return [list $result [$result getElementsByTagName entry]]
+ }
+}
+
+set gdocs(upload) {
+ url http://docs.google.com/feeds/default/private/full
+ method post
+ body mime_multipart
+ headers {
+ GData-Version 3.0
+ Authorization {GoogleLogin auth=%token%}
+ }
+ opt_args { ocr: }
+}
+
+set gdocs(export) {
+ url http://docs.google.com/feeds/download/documents/Export
+ headers {
+ GData-Version 3.0
+ Authorization {GoogleLogin auth=%token%}
+ }
+ body { arg docID }
+ opt_args { exportFormat: }
+ format raw
+}
+
+rest::create_interface gdocs
+
+proc ::gdocs::create_doc_metadata {args} {
+ set defaults [dict create \
+ text ""
+ ]
+ set args [lindex [::rest::parse_opts {} {title:} {} $args] 0]
+ set args [dict merge $defaults $args]
+
+ set xml {}
+ lappend xml "<?xml version='1.0' encoding='UTF-8'?>"
+ lappend xml "<entry xmlns='http://www.w3.org/2005/Atom' xmlns:gd='http://schemas.google.com/g/2007'>"
+ lappend xml "<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event'/>"
+ lappend xml "<title>[dict get $args title]</title>"
+ lappend xml "</entry>"
+ return [join $xml \n]
+}
+
+
+# Example usage
+#source gdocs
+#set auth [gdocs::auth -Email me@gmail.com -Passwd passwd]
+#gdocs::set_static_args -token $auth
+#
+#set file IMG_0848.jpg
+#set meta [gdocs::create_doc_metadata -title [file rootname $file]]
+#
+#set fh [open $file]
+#fconfigure $fh -encoding binary -translation lf
+#set filedata [read $fh]
+#close $fh
+#
+#gdocs::upload -ocr true -- [list {Content-Type application/atom+xml} $meta] [list {Content-Type image/jpeg} $filedata]
+
diff --git a/tcllib/modules/rest/pkgIndex.tcl b/tcllib/modules/rest/pkgIndex.tcl
new file mode 100644
index 0000000..fe782df
--- /dev/null
+++ b/tcllib/modules/rest/pkgIndex.tcl
@@ -0,0 +1,2 @@
+if {![package vsatisfies [package provide Tcl] 8.5]} {return}
+package ifneeded rest 1.0.2 [list source [file join $dir rest.tcl]]
diff --git a/tcllib/modules/rest/rest.man b/tcllib/modules/rest/rest.man
new file mode 100644
index 0000000..422656b
--- /dev/null
+++ b/tcllib/modules/rest/rest.man
@@ -0,0 +1,538 @@
+[comment {-*- tcl -*- doctools manpage}]
+[vset VERSION 1.0.2]
+[manpage_begin rest n [vset VERSION]]
+[moddesc {A framework for RESTful web services}]
+[titledesc {define REST web APIs and call them inline or asychronously}]
+[require Tcl 8.5]
+[require rest [opt [vset VERSION]]]
+[description]
+
+There are two types of usage this package supports: [term {simple calls}],
+and complete [term interfaces].
+
+In an [term interface] you specify a set of rules and then the package
+builds the commands which correspond to the REST methods. These
+commands can have many options such as input and output
+transformations and data type specific formatting. This results in a
+cleaner and simpler script.
+
+On the other hand, while a [term {simple call}] is easier and quicker
+to implement it is also less featureful. It takes the url and a few
+options about the command and returns the result directly. Any
+formatting or checking is up to rest of the script.
+
+[section {Simple usage}]
+
+In simple usage you make calls using the http method procedures and
+then check or process the returned data yourself
+
+[comment {= = == === ===== ======== ============= =====================}]
+[comment {= = == === ===== ======== ============= =====================}]
+[list_begin definitions]
+[call [cmd ::rest::simple] [arg url] [arg query] [opt [arg config]] [opt [arg body]]]
+[call [cmd ::rest::get] [arg url] [arg query] [opt [arg config]] [opt [arg body]]]
+[call [cmd ::rest::post] [arg url] [arg query] [opt [arg config]] [opt [arg body]]]
+[call [cmd ::rest::head] [arg url] [arg query] [opt [arg config]] [opt [arg body]]]
+[call [cmd ::rest::put] [arg url] [arg query] [opt [arg config]] [opt [arg body]]]
+[call [cmd ::rest::delete] [arg url] [arg query] [opt [arg config]] [opt [arg body]]]
+
+[para] These commands are all equivalent except for the http method
+used.
+
+If you use [cmd simple] then the method should be specified as an
+option in the [arg config] dictionary. If that is not done it defaults
+to [const get]. If a [arg body] is needed then the [arg config]
+dictionary must be present, however it is allowed to be empty.
+
+[para] The [arg config] dictionary supports the following keys
+
+[list_begin definitions]
+[def [const auth]]
+[def [const content-type]]
+[def [const cookie]]
+[def [const format]]
+[def [const headers]]
+[def [const method]]
+
+[comment {-- TODO -- describe the meaning of the various keys -- }]
+[list_end]
+
+[para] Two quick examples:
+
+[para] Example 1, Yahoo Boss:
+[comment {--- --- --- --- -- ---- --- --- ---}][example {
+ set appid APPID
+ set search tcl
+ set res [rest::get http://boss.yahooapis.com/ysearch/web/v1/$search [list appid $appid]]
+ set res [rest::format_json $res]
+}][comment {--- --- --- --- -- ---- --- --- ---}]
+
+[para] Example 2, Twitter:
+[comment {--- --- --- --- -- ---- --- --- ---}][example {
+ set url http://twitter.com/statuses/update.json
+ set query [list status $text]
+ set res [rest::simple $url $query {
+ method post
+ auth {basic user password}
+ format json
+ }]
+}][comment {--- --- --- --- -- ---- --- --- ---}]
+
+[list_end]
+
+[section {Interface usage}]
+
+An interface to a REST API consists of a series of definitions of REST
+calls contained in an array.
+
+The name of that array becomes a namespace containing the defined
+commands. Each key of the array specifies the name of the call, with
+the associated configuration a dictionary, i.e. key/value pairs.
+
+The acceptable keys, i.e. legal configuration options are described
+below.
+
+After creating the definitions in the array simply calling
+[cmd rest::create_interface] with the array as argument will then
+create the desired commands.
+
+[para] Example, Yahoo Weather:
+[comment {--- --- --- --- --- --- --- --- ---}][example {
+ package require rest
+
+ set yweather(forecast) {
+ url http://weather.yahooapis.com/forecastrss
+ req_args { p: }
+ opt_args { u: }
+ }
+ rest::create_interface yweather
+ puts [yweather::forecast -p 94089]
+}][comment {--- --- --- --- -- ---- --- --- ---}]
+
+[comment { -- TODO -- figure out what these are ! standard methods ?
+::${name}::set_static_args [opt args]]
+}]
+
+[comment {= = == === ===== ======== ============= =====================}]
+[comment {= = == === ===== ======== ============= =====================}]
+[list_begin definitions]
+
+[comment {= = == === ===== ======== ============= =====================}]
+[call [cmd ::rest::save] [arg name] [arg file]]
+
+This command saves a copy of the dynamically created procedures for
+all the API calls specified in the array variable [arg name] to the
+[arg file], for later loading.
+
+[para] The result of the command is the empty string
+
+[comment {= = == === ===== ======== ============= =====================}]
+[call [cmd ::rest::describe] [arg name]]
+
+This command prints a description of all API calls specified in the array
+variable [arg name] to the channel [const stdout].
+
+[para] The result of the command is the empty string.
+
+[comment {= = == === ===== ======== ============= =====================}]
+[call [cmd ::rest::parameters] [arg url] [opt [arg key]]]
+
+This command parses an [arg url] query string into a dictionary and
+returns said dictionary as its result.
+
+[para] If [arg key] is specified the command will not return the
+entire dictionary, but only the value of that [arg key].
+
+[comment {= = == === ===== ======== ============= =====================}]
+[call [cmd ::rest::parse_opts] [arg static] [arg required] [arg optional] [arg words]]
+
+This command implements a custom parserfor command options.
+
+[list_begin arguments]
+[arg_def dict static]
+A dictionary of options and their values that are always present in
+the output.
+
+[arg_def list required]
+A list of options that must be supplied by [arg words]
+
+[arg_def list optional]
+A list of options that may appear in the [arg words], but are not required.
+The elements must be in one of three forms:
+
+[list_begin definitions]
+[def name] The option may be present or not, no default.
+[def name:] When present the option requires an argument.
+[def name:value] When not present use [const value] as default.
+[list_end]
+
+[arg_def list words]
+The words to parse into options and values.
+
+[list_end]
+
+[para] The result of the command is a list containing two elements.
+The first element is a dictionary containing the parsed options and
+their values. The second element is a list of the remaining words.
+
+[call [cmd ::rest::substitute] [arg string] [arg var]]
+
+This command takes a [arg string], substitutes values for any option
+identifiers found inside and returns the modified string as its
+results.
+
+[para] The values to substitute are found in the variable [arg var],
+which is expected to contain a dictionary mapping from the option
+identifiers to replace to their values.
+
+[emph Note] that option identifiers which have no key in [arg var] are
+replaced with the empty string.
+
+[para] The option identifiers in [arg string] have to follow the
+syntax [const %...%] where [var ...] may contain any combination of
+lower-case alphanumeric characters, plus underscore, colon and dash.
+
+[comment {= = == === ===== ======== ============= =====================}]
+[call [cmd ::rest::create_interface] [arg name]]
+
+This command creates procedures for all the API calls specified in the
+array variable [arg name].
+
+[para] The name of that array becomes a namespace containing the defined
+commands. Each key of the array specifies the name of the call, with
+the associated configuration a dictionary, i.e. key/value pairs.
+
+The legal keys and their meanings are:
+
+[list_begin definitions]
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const url]]
+
+The value of this [emph required] option must be the target of the
+http request.
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const description]]
+
+The value of this option must be a short string describing the call.
+Default to the empty string, if not specified.
+Used only by [cmd ::rest::describe].
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const body]]
+
+The value of this option indicates if arguments are required for the
+call's request body or not. The acceptable values are listed below.
+
+Defaults to [const optional] if not specified.
+
+[list_begin definitions]
+[def [const none]]
+The call has no request body, none must be supplied.
+[def [const optional]]
+A request body can be supplied, but is not required.
+
+[def [const required]]
+A request body must be supplied.
+
+[def [const argument]]
+
+This value must be followed by the name of an option, treating the
+entire string as a list. The request body will be used as the value of
+that option.
+
+[def [const mime_multipart]]
+
+A request body must be supplied and will be interpreted as each
+argument representing one part of a mime/multipart document.
+
+Arguments must be lists containing 2 elements, a list of header keys
+and values, and the mime part body, in this order.
+
+[list_end]
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const method]]
+
+The value of this option must be the name of the HTTP method to call
+on the url.
+
+Defaults to GET, if not specified.
+
+The acceptable values are [const GET], [const POST], and [const PUT],
+regardless of letter-case.
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const copy]]
+
+When present the value of this option specifies the name of a
+previously defined call. The definition of that call is copied to the
+current call, except for the options specified by the current call
+itself.
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const unset]]
+
+When present the value of this option names an option in the current
+call. This option is removed from the definition. Use this after
+[const copy]ing an existing definition to remove options, instead of
+overriding their value.
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const headers]]
+
+Specification of additional header fields. The value of this option
+must be a dictionary, interpreted to contain the new header fields and
+their values. The default is to not add any additional headers.
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const content-type]]
+
+The value of this option specifies the content type for the request data.
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const req_args]]
+
+The value of this option is a list naming the required arguments of
+the call. Names ending in a colon will require a value.
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const opt_args]]
+
+The value of this option a list naming the arguments that may be
+present for a call but are not required.
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const static_args]]
+
+The value of this option a list naming the arguments that are always
+the same. No sense in troubling the user with these. A leading dash
+([const -]) is allowed but not required to maintain consistency with
+the command line.
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const auth]]
+
+The value of this option specifies how to authenticate the calls.
+No authentication is done if the option is not specified.
+
+[list_begin definitions]
+[def [const basic]]
+
+The user may configure the [term {basic authentication}] by overriding
+the procedure [cmd basic_auth] in the namespace of interface. This
+procedure takes two arguments, the username and password, in this
+order.
+
+[def [const sign]]
+
+The value must actually be a list with the second element the name of
+a procedure which will be called to perform request signing.
+
+[list_end]
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const callback]]
+
+If this option is present then the method will be created as an
+[term async] call. Such calls will return immediately with the value
+of the associated http token instead of the call's result. The event
+loop must be active to use this option.
+
+[para] The value of this option is the name of a procedure which is
+invoked when the HTTP call is complete. The procedure will receive
+three arguments, the name of the calling procedure, the status of the
+result (one of [const OK] or [const ERROR]), and the data associated
+with the result, in this order.
+
+The http request header will be available in that procedure via
+[cmd {upvar token token}].
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const cookie]]
+
+The value of this option is a list of cookies to be passed in the http
+header. This is a shortcut to the [const headers] option.
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const input_transform]]
+
+The value of this option is a command prefix or script to perform a
+transformation on the query before invoking the call. A script
+transform is wrapped into an automatically generated internal
+procedure.
+
+[para] If not specified no transformation is done.
+
+[para] The command (prefix) must accept a single argument, the query
+(a dictionary) to transform, and must return the modified query (again
+as dictionary) as its result.
+
+The request body is accessible in the transform command via
+[cmd {upvar body body}].
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const format]]
+[def [const result]]
+
+The value of this option specifies the format of the returned
+data.
+
+Defaults to [const auto] if not specified.
+
+The acceptable values are:
+[list_begin definitions]
+[def [const auto]]
+Auto detect between [const xml] and [const json].
+[def [const discard]]
+[def [const json]]
+[def [const raw]]
+[def [const rss]]
+This is formatted as a special case of [const xml].
+[def [const tdom]]
+[def [const xml]]
+[list_end]
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const pre_transform]]
+
+The value of this option is a command prefix or script to perform a
+transformation on the result of a call ([emph before] the application
+of the output transform as per [const format]). A script transform is
+wrapped into an automatically generated internal procedure.
+
+[para] If not specified no transformation is done.
+
+[para] The command (prefix) must accept a single argument, the result
+to transform, and must return the modified result as its result.
+
+[para] The http request header is accessible in the transform command
+via [cmd {upvar token token}]
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const post_transform]]
+
+The value of this option is a command prefix or script to perform a
+transformation on the result of a call ([emph after] the application
+of the output transform as per [const format]). A script transform is
+wrapped into an automatically generated internal procedure.
+
+[para] If not specified no transformation is done.
+
+[para] The command (prefix) must accept a single argument, the result
+to transform, and must return the modified result as its result.
+
+[para] The http request header is accessible in the transform command
+via [cmd {upvar token token}]
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[def [const check_result]]
+
+The value of this option must be list of two expressions, either of
+which may be empty.
+
+[para] The first expression is checks the OK condition, it must return
+[const true] when the result is satisfactory, and [const false]
+otherwise.
+
+[para] The second expression is the ERROR condition, it must return
+[const false] unless there is an error, then it has to return
+[const true].
+
+[comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@]
+[list_end]
+
+[list_end][comment {-- end of command list --}]
+
+[section Examples]
+
+[para] Yahoo Geo:
+[comment {--- --- --- --- -- ---- --- --- ---}][example {
+set ygeo(parse) {
+ url http://wherein.yahooapis.com/v1/document
+ method post
+ body { arg documentContent }
+}
+ygeo::parse "san jose ca"
+# "san jose ca" will be interpreted as if it were specified as the -documentContent option
+}][comment {--- --- --- --- -- ---- --- --- ---}]
+
+[para] Google Docs:
+[comment {--- --- --- --- -- ---- --- --- ---}][example {
+set gdocs(upload) {
+ url http://docs.google.com/feeds/default/private/full
+ body mime_multipart
+}
+gdocs::upload [list {Content-Type application/atom+xml} $xml] [list {Content-Type image/jpeg} $filedata]
+}][comment {--- --- --- --- -- ---- --- --- ---}]
+
+[para] Delicious:
+[comment {--- --- --- --- -- ---- --- --- ---}][example {
+set delicious(updated) {
+ url https://api.del.icio.us/v1/posts/update
+ auth basic
+}
+
+rest::create_interface flickr
+
+flickr::basic_auth username password
+}][comment {--- --- --- --- -- ---- --- --- ---}]
+
+[para] Flickr:
+[comment {--- --- --- --- -- ---- --- --- ---}][example {
+set flickr(auth.getToken) {
+ url http://api.flickr.com/services/rest/
+ req_args { api_key: secret: }
+ auth { sign do_signature }
+}
+
+rest::create_interface flickr
+
+proc ::flickr::do_signature {query} {
+ # perform some operations on the query here
+ return $query
+}
+}][comment {--- --- --- --- -- ---- --- --- ---}]
+
+[section INCLUDED]
+
+The package provides functional but incomplete implementations for the following services:
+
+[list_begin definitions]
+[def [const del.icio.us]]
+[def [const facebook]]
+[def [const flickr]]
+[def [const twitter]]
+[def [const {google calendar}]]
+[def [const {yahoo boss}]]
+ [def [const {yahoo weather}]]
+[list_end]
+
+Please either read the package's implementation, or use
+[cmd rest::describe] after loading it for their details.
+
+[para] Do not forget developers' documentation on the respective sites either.
+
+[section TLS]
+
+The [package rest] package can be used with [term https]-secured
+services, by requiring the [package TLS] package and then registering
+it with the [package http] package it is sitting on top of.
+
+Example
+[example {
+ package require tls
+ http::register https 443 ::tls::socket
+}]
+
+[include ../common-text/tls-security-notes.inc]
+
+[vset CATEGORY rest]
+[include ../doctools2base/include/feedback.inc]
+[comment {
+TOKENS
+ the value is substituted into the url at call time.
+ tokens in the form of %name:default_value% will be
+ an optional argument with a default value.
+}]
+[manpage_end]
diff --git a/tcllib/modules/rest/rest.tcl b/tcllib/modules/rest/rest.tcl
new file mode 100644
index 0000000..64f717d
--- /dev/null
+++ b/tcllib/modules/rest/rest.tcl
@@ -0,0 +1,829 @@
+# rest.tcl --
+#
+# A framework for RESTful web services
+#
+# Copyright (c) 2009 Aaron Faupell
+#
+# RCS: @(#) $Id: rest.tcl,v 1.7 2009/10/14 16:28:18 afaupell Exp $
+
+package require Tcl 8.5
+package require http 2.7
+package require json
+package require tdom
+package require base64
+
+package provide rest 1.0.2
+
+namespace eval ::rest {
+ namespace export create_interface parameters parse_opts save \
+ describe substitute
+}
+
+# simple --
+#
+# perform a simple rest call
+#
+# ARGS:
+# url name of the array containing command definitions
+# query query string or list of key/value pairs to be passed to http::formatQuery
+# config (optional) dict containing configuration options for the call
+# body (optional) data for the body of the http request
+#
+# RETURNS:
+# the data from the rest call
+#
+proc ::rest::simple {url query args} {
+ set headers [list]
+ set config [lindex $args 0]
+ if {[string index $config 0] == "-"} {
+ set opts [parse_opts {} {} {headers: cookie: auth: format: method:} [join $args]]
+ set config [lindex $opts 0]
+ set body [lindex $opts 1]
+ } else {
+ set body [lindex $args 1]
+ }
+
+ DetermineMethod config
+
+ if {[string first " " $query] > 0} {
+ # if query has a space assume it is a list of key value pairs, and do the formatting
+ set query [eval ::http::formatQuery $query]
+ } elseif {[string first ? $url] > 0 && $query == ""} {
+ # if the url contains a query string and query empty then split it to the correct vars
+ set query [join [lrange [split $url ?] 1 end] ?]
+ set url [lindex [split $url ?] 0]
+ }
+
+ if {[dict exists $config auth]} {
+ set auth [dict get $config auth]
+ if {[lindex $auth 0] == "basic"} {
+ lappend headers Authorization "Basic [base64::encode [lindex $auth 1]:[lindex $auth 2]]"
+ }
+ }
+ if {[dict exists $config content-type]} {
+ lappend headers Content-type [join [dict get $config content-type] \;]
+ }
+ if {[dict exists $config headers]} {
+ dict for {key val} [dict get $config headers] { lappend headers $key $val }
+ }
+ if {[dict exists $config cookie]} {
+ lappend headers Cookie [join [dict get $config cookie] \;]
+ }
+
+ set result [::rest::_call {} $headers $url $query $body]
+
+ # if a format was specified then convert the data, but dont do any auto formatting
+ if {[dict exists $config result]} {
+ set result [::rest::format_[dict get $config result] $result]
+ }
+
+ return $result
+}
+
+interp alias {} ::rest::get {} ::rest::simple
+interp alias {} ::rest::post {} ::rest::simple
+interp alias {} ::rest::head {} ::rest::simple
+interp alias {} ::rest::put {} ::rest::simple
+interp alias {} ::rest::delete {} ::rest::simple
+
+proc ::rest::DetermineMethod {cv} {
+ upvar 1 $cv config
+ if {[dict exists $config method]} return
+
+ set loc [info frame -2]
+ if {![dict exists $loc cmd]} {
+ return -code error "Unable to determine rest::simple method in the current context ([dict get $loc type]). Please specify it explicitly."
+ }
+ set cmd [dict get $loc cmd]
+ if {[catch {
+ set cmd [lindex $cmd 0]
+ }]} {
+ # Not a proper list. String processing.
+ # Simple: Assume name without spaces.
+ # TODO: Quoted literal.
+ regexp {^([^ ]+).*$} $cmd -> cmd
+ }
+ if {$cmd ni {get delete head post put}} {
+ return -code error "Unable to determine rest::simple method, found \"$cmd\". Please specify it explicitly."
+ }
+ set cmd [namespace tail $cmd]
+ if {$cmd eq "simple"} { set cmd get }
+ #puts >>>|$cmd|
+ dict set config method $cmd
+ return
+}
+
+# create_interface --
+#
+# use an array which defines a rest API to construct a set of procs
+#
+# ARGS:
+# name name of the array containing command definitions
+#
+# EFFECTS:
+# creates a new namespace and builds api procedures within it
+#
+# RETURNS:
+# the name of the new namespace, which is the same as the input name
+#
+proc ::rest::create_interface {name} {
+ upvar $name in
+
+ # check if any defined calls have https urls and automatically load and register tls
+ #if {[catch {package present tls}]} {
+ # foreach x [array names in] {
+ # if {[dict exists $in($x) url] && [string match https://* [dict get $in($x) url]]} {
+ # package require tls
+ # ::http::register https 443 [list ::tls::socket]
+ # break
+ # }
+ # }
+ #}
+
+ namespace eval ::$name {}
+ foreach call [array names in] {
+ set config $in($call)
+ set proc [list]
+
+ if {[dict exists $config copy]} {
+ set config [dict merge $in([dict get $config copy]) $config]
+ }
+ if {[dict exists $config unset]} {
+ set config [eval [list dict remove $config] [dict get $config unset]]
+ }
+ if {[dict exists $config content-type]} {
+ dict set config headers content-type [dict get $config content-type]
+ }
+
+ lappend proc "set config \{$config\}"
+ lappend proc "set headers \{\}"
+
+ # invocation option processing
+ _addopts [dict get $config url] config
+ if {[dict exists $config headers]} {
+ dict for {k val} [dict get $config headers] {
+ _addopts $val config
+ }
+ }
+ set opts [list]
+ lappend proc "set static \{[expr {[dict exists $config static_args] ? [dict get $config static_args] : {}}]\}"
+ lappend proc {variable static_args}
+ lappend proc {if {[info exists static_args]} { set static [dict merge $static $static_args] }}
+ lappend opts [expr {[dict exists $config req_args] ? [dict get $config req_args] : ""}]
+ lappend opts [expr {[dict exists $config opt_args] ? [dict get $config opt_args] : ""}]
+ lappend proc "set parsed \[::rest::parse_opts \$static $opts \$args]"
+ lappend proc {set query [lindex $parsed 0]}
+ lappend proc {set body [lindex $parsed 1]}
+ lappend proc {set url [::rest::substitute [dict get $config url] query]}
+ if {[dict exists $config body]} {
+ if {[string match req* [dict get $config body]]} {
+ lappend proc {if {$body == ""} { return -code error "wrong # args: should be \"[lindex [info level 0] 0] ?options? string\"" }}
+ } elseif {[string match no* [dict get $config body]]} {
+ lappend proc {if {$body != ""} { return -code error "extra arguments after options" }}
+ } elseif {[string match arg* [lindex [dict get $config body] 0]]} {
+ lappend proc {if {$body == ""} { return -code error "wrong # args: should be \"[lindex [info level 0] 0] ?options? string\"" }}
+ lappend proc "lappend query [lindex [dict get $config body] 1] \$body" {set body ""}
+ } elseif {[string match mime_multi* [lindex [dict get $config body] 0]]} {
+ lappend proc {if {$body == ""} { return -code error "wrong # args: should be \"[lindex [info level 0] 0] ?options? string\"" }}
+ lappend proc {set b [::rest::mime_multipart body $body]}
+ lappend proc {dict set config headers content-type "multipart/related; boundary=$b"}
+ }
+ }
+ # end option processing
+
+ if {[dict exists $config auth]} {
+ set auth [dict get $config auth]
+ if {$auth == "basic"} {
+ lappend proc "lappend headers Authorization \"Basic \[base64::encode \$\{::${name}::user\}:\$\{::${name}::password\}]\""
+ if {[info commands ::${name}::basic_auth] == ""} {
+ proc ::${name}::basic_auth {u p} {
+ variable user $u
+ variable password $p
+ }
+ }
+ }
+ }
+
+ if {[dict exists $config headers]} {
+ lappend proc {dict for {key val} [dict get $config headers] { lappend headers $key [::rest::substitute $val query] }}
+ }
+ if {[dict exists $config cookie]} {
+ lappend proc {lappend headers Cookie [join [dict get $config cookie] \;]}
+ }
+ _transform $name $call $config proc input_transform query
+ if {[dict exists $config auth] && [lindex [dict get $config auth] 0] == "sign"} {
+ lappend proc "set query \[::${name}::[lindex [dict get $config auth] 1] \$query]"
+ }
+
+ lappend proc {set query [eval ::http::formatQuery $query]}
+
+ # if this is an async call (has defined a callback)
+ # then end the main proc here by returning the http token
+ # the rest of the normal result processing will be put in a _callback_NAME
+ # proc which is called by the generic _callback proc
+ if {[dict exists $config callback]} {
+ lappend proc "set t \[::rest::_call \{[list ::${name}::_callback_$call [dict get $config callback]]\} \$headers \$url \$query \$body]"
+ lappend proc {return $t}
+ proc ::${name}::$call args [join $proc \n]
+ set proc {}
+ lappend proc {upvar token token}
+ } else {
+ lappend proc {set result [::rest::_call {} $headers $url $query $body]}
+ }
+
+ # process results
+ _transform $name $call $config proc pre_transform result
+ if {[dict exists $config result]} {
+ lappend proc "set result \[::rest::format_[dict get $config result] \$result]"
+ } elseif {[dict exists $config format]} {
+ lappend proc "set result \[::rest::format_[dict get $config format] \$result]"
+ } else {
+ lappend proc "set result \[::rest::format_auto \$result]"
+ }
+ _transform $name $call $config proc post_transform result
+ if {[dict exists $config check_result]} {
+ lappend proc "::rest::_check_result \$result [dict get $config check_result]"
+ }
+ # end process results
+
+ # if this is an async call (has a defined callback)
+ # create the callback proc which contains only the result processing and
+ # a handoff to the user defined callback
+ # otherwise create the normal call proc
+ if {[dict exists $config callback]} {
+ lappend proc "[dict get $config callback] $call OK \$result"
+ proc ::${name}::_callback_$call {result} [join $proc \n]
+ } else {
+ lappend proc {return $result}
+ proc ::${name}::$call args [join $proc \n]
+ }
+ }
+
+ proc ::${name}::set_static_args {args} {
+ variable static_args
+ set static_args $args
+ }
+
+ set ::${name}::static_args {}
+
+ # print the contents of all the dynamic generated procs
+ if {0} {
+ foreach x [info commands ::${name}::*] {
+ puts "proc $x \{[info args $x]\} \{\n[info body $x]\n\}\n"
+ }
+ }
+ return $name
+}
+
+# mime_multipart --
+#
+# creates a mime mulipart message
+#
+# ARGS:
+# var name of variable in which the mime body is stored
+# body a list of key/value pairs which represent mime part
+# headers and bodies. the header is itself a list of
+# value pairs which define header fields
+#
+# EFFECTS:
+# replaces $var with a mime body
+#
+# RETURNS:
+# the mime boundary string
+#
+proc ::rest::mime_multipart {var body} {
+ upvar $var out
+ set out {}
+ set boundary _000-MIME_SEPERATOR
+ foreach {head data} $body {
+ append out \n--$boundary\n
+ foreach {k v} $head {
+ append out "$k: $v\n"
+ }
+ append out \n$data\n
+ }
+ append out \n--$boundary--\n
+ return $boundary
+}
+
+# _transform --
+#
+# called by create_interface to handle the creation of user defined procedures
+#
+# ARGS:
+# ns target namespace
+# call name of the proc that is being created
+# config dict of config options
+# proc name of variable holding the proc being created
+# name name of the transform
+#
+# EFFECTS:
+# appends commands to the proc variable and possible creates a new proc
+#
+# RETURNS:
+# nothing
+#
+proc ::rest::_transform {ns call config proc name var} {
+ upvar $proc p
+ if {[dict exists $config $name]} {
+ set t [dict get $config $name]
+ if {[llength [split $t]] == 1 && [info commands $t] != ""} {
+ lappend p "set $var \[$t \$$var]"
+ } else {
+ lappend p "set $var \[::${ns}::_${name}_$call \$$var]"
+ proc ::${ns}::_${name}_$call $var $t
+ }
+ }
+}
+
+# save --
+#
+# saves a copy of the dynamically created procs to a file for later loading
+#
+# ARGS:
+# name name of the array containing command definitions
+# file name of file in which to save the generated commands
+#
+# RETURNS:
+# nothing
+#
+proc ::rest::save {name file} {
+ set fh [open $file w]
+ puts $fh {package require http
+package require json
+package require tdom
+package require base64
+}
+
+ if {![catch {package present tls}]} {
+ puts $fh {
+package require tls
+::http::register https 443 [list ::tls::socket]
+}
+ }
+
+ puts $fh "namespace eval ::$name \{\}\n"
+ foreach x {_call _callback parse_opts _addopts substitute _check_result \
+ format_auto format_raw format_xml format_json format_discard \
+ format_tdom} {
+ puts $fh "proc ::${name}::$x \{[info args $x]\} \{[info body $x]\n\}\n"
+ }
+ foreach x [info commands ::${name}::*] {
+ puts $fh "proc $x \{[info args $x]\} \{\n[info body $x]\n\}\n"
+ }
+ close $fh
+}
+
+# parameters --
+#
+# parse a url query string into a dict
+#
+# ARGS:
+# url a url with a query string seperated by a '?'
+# args optionally a dict key to return instead of the entire dict
+#
+# RETURNS:
+# a dict containing the parsed query string
+#
+proc ::rest::parameters {url args} {
+ set dict [list]
+ foreach x [split [lindex [split $url ?] 1] &] {
+ set x [split $x =]
+ if {[llength $x] < 2} { lappend x "" }
+ eval lappend dict $x
+ }
+ if {[llength $args] > 0} {
+ return [dict get $dict [lindex $args 0]]
+ }
+ return $dict
+}
+
+# _call --
+#
+# makes an http request
+# expected to be called only by a generated procedure because it depends on the
+# config dict
+#
+# ARGS:
+# name name of the array containing command definitions
+# callback empty string, or a list of 2 callback procs,
+# generated and user defined. if not empty the call will
+# be made async (-command argument to geturl)
+# headers a dict of keys/values for the http request header
+# url the url to request
+# query
+# body
+#
+# EFFECTS:
+# creates a new namespace and builds api procedures within it
+#
+# RETURNS:
+# the data from the http reply, or an http token if the request was async
+#
+proc ::rest::_call {callback headers url query body} {
+ #puts "_call [list $callback $headers $url $query $body]"
+ # get the settings from the calling proc
+ upvar config config
+
+ set method GET
+ if {[dict exists $config method]} { set method [string toupper [dict get $config method]] }
+
+ # assume the query should really be the body for post or put requests
+ # with no other body. doesnt seem technically correct but works for
+ # everything I have encountered. there is no way for the call definition to
+ # specify the difference between url parameters and request body
+ if {[dict exists $config body] && [string match no* [dict get $config body]]} {
+ # never put the query in the body if the user said no body
+ } elseif {($method == "POST" || $method == "PUT") && $query != "" && $body == ""} {
+ set body $query
+ set query {}
+ }
+ if {$query != ""} { append url ?$query }
+
+ # configure options to the geturl command
+ set opts [list]
+ lappend opts -method $method
+ if {[dict exists $headers content-type]} {
+ lappend opts -type [dict get $headers content-type]
+ set headers [dict remove $headers content-type]
+ }
+ if {$body != ""} {
+ lappend opts -query $body
+ }
+ if {$callback != ""} {
+ lappend opts -command [list ::rest::_callback {*}$callback]
+ }
+
+ #puts "headers $headers"
+ #puts "opts $opts"
+ #puts "geturl $url"
+ #return
+ set t [http::geturl $url -headers $headers {*}$opts]
+
+ # if this is an async request return now, otherwise process the result
+ if {$callback != ""} { return $t }
+ if {![string match 2* [http::ncode $t]]} {
+ #parray $t
+ if {[string match {30[123]} [http::ncode $t]]} {
+ upvar #0 $t a
+ return -code error [list HTTP [http::ncode $t] [dict get $a(meta) Location]]
+ }
+ return -code error [list HTTP [http::ncode $t]]
+ }
+ set data [http::data $t]
+ # copy the token into the calling scope so that the transforms can access it
+ # via uplevel, and we can still call cleanup on the real token
+ upvar token token
+ array set token [array get $t]
+
+ #parray $t
+ #puts "data: $data"
+ http::cleanup $t
+ return $data
+}
+
+# _callback --
+#
+# callback procedure for async http requests
+#
+# ARGS:
+# datacb name of the dynamically generated callback proc created by
+# create_interface which contains post transforms and content
+# interpreting
+# usercb the name of the user supplied callback function.
+# if there is an error it is called directly from here,
+# otherwise the datacb calls it
+# t the http request token
+#
+# EFFECTS:
+# evaluates http error conditions and calls the user defined callback
+#
+# RETURNS:
+# nothing
+#
+proc ::rest::_callback {datacb usercb t} {
+ # copy the token into the local scope so that the datacb can access it
+ # via uplevel, and we can still call cleanup on the real token
+ array set token [array get $t]
+ if {![string match 2* [http::ncode $t]]} {
+ set data [list HTTP [http::ncode $t]]
+ if {[http::ncode $t] == "302"} {
+ lappend data [dict get $token(meta) Location]
+ }
+ http::cleanup $t
+ $usercb ERROR $data
+ return
+ }
+ set data [http::data $t]
+ http::cleanup $t
+ eval $datacb [list $data]
+}
+
+# parse_opts --
+#
+# command option parsing
+#
+# ARGS:
+# static a dict of options and values that are always present
+# required a list of options that must be supplied
+# optional a list of options that may appear but are not required
+# the format is
+# name - an option which is present or not, no default
+# name: - an option which requires a value
+# name:value - an option with a default value
+# options the string of options supplied by the user at invocation
+#
+# EFFECTS:
+# none
+#
+# RETURNS:
+# a 2 item list. the first item is a dict containing the parsed
+# options and their values. the second item is a string of any
+# remaining data
+# ex: [list [dict create opt1 value1 opt2 value2] {some extra text supplied to the command}]
+#
+proc ::rest::parse_opts {static required optional options} {
+ #puts "static $static\nrequired $required\noptional $optional\noptions $options"
+ set args $options
+ set query {}
+ foreach {k v} $static {
+ set k [string trimleft $k -]
+ lappend query $k $v
+ }
+
+ foreach opt $required {
+ if {[string index $opt end] == ":"} {
+ set opt [string range $opt 0 end-1]
+ }
+ if {[set i [lsearch -exact $args -$opt]] >= 0} {
+ if {[llength $args] <= $i+1} { return -code error "the -$opt argument requires a value" }
+ lappend query $opt [lindex $args [expr {$i+1}]]
+ set args [lreplace $args $i [expr {$i+1}]]
+ } elseif {[set i [lsearch -regexp $static ^-?$opt$]] >= 0} {
+ lappend query $opt [lindex $static [expr {$i+1}]]
+ set static [lreplace $static $i [expr {$i+1}]]
+ } else {
+ return -code error "the -$opt argument is required"
+ }
+ }
+
+ while {[llength $args] > 0} {
+ set opt [lindex $args 0]
+ if {![string match -* $opt]} break
+ if {$opt == "--"} {
+ set args [lreplace $args 0 0]
+ break
+ }
+ set opt [string range $opt 1 end]
+
+ if {[set i [lsearch $optional $opt:*]] > -1} {
+ lappend query $opt [lindex $args 1]
+ set args [lreplace $args 0 1]
+ set optional [lreplace $optional $i $i]
+ } elseif {[set i [lsearch -exact $optional $opt]] > -1} {
+ lappend query $opt ""
+ set args [lreplace $args 0 0]
+ set optional [lreplace $optional $i $i]
+ } else {
+ set opts {}
+ foreach x [concat $required $optional] { lappend opts -[string trimright $x :] }
+ if {[llength $opts] > 0} {
+ return -code error "bad option \"$opt\": Must be [join $opts ", "]"
+ }
+ return -code error "bad option \"$opt\""
+ }
+ }
+
+ foreach opt $optional {
+ if {[set i [lsearch -regexp $static ^-?$opt$]] >= 0} {
+ lappend query $opt [lindex $static [expr {$i+1}]]
+ set static [lreplace $static $i [expr {$i+1}]]
+ } elseif {[string match *:?* $opt]} {
+ set opt [split $opt :]
+ lappend query [lindex $opt 0] [join [lrange $opt 1 end]]
+ }
+ }
+ #puts "optional $optional\nquery $query"
+ return [list $query [join $args]]
+}
+
+# _addopts --
+#
+# add inline argument identifiers to the options list
+#
+# ARGS:
+# str a string which may contain %word% option identifiers
+# c name of the config variable
+#
+# EFFECTS:
+# modifies the option variable to add any identifiers found
+#
+# RETURNS:
+# nothing
+#
+proc ::rest::_addopts {str c} {
+ upvar $c config
+ foreach {junk x} [regexp -all -inline -nocase {%([a-z0-9_:-]+)%} $str] {
+ if {[string match *:* $x]} {
+ dict lappend config opt_args $x
+ } else {
+ dict lappend config req_args $x:
+ }
+ }
+}
+
+# substitute --
+#
+# take a string and substitute real values for any option identifiers
+#
+# ARGS:
+# input a string which may contain %word% option identifiers
+# q name of a variable containing a dict of options and values
+#
+# EFFECTS:
+# removes any substituted options from the q variable
+#
+# RETURNS:
+# the input string with option identifiers replaced by real values
+#
+proc ::rest::substitute {input q} {
+ upvar $q query
+ foreach {junk name} [regexp -all -inline -nocase {%([a-z0-9_:-]+)%} $input] {
+ set opt [lindex [split $name :] 0]
+ if {[dict exists $query $opt]} {
+ set replace [dict get $query $opt]
+ #set replace [string map {/ %2F} $replace]
+ #set replace [string range [http::formatQuery "" $replace] 1 end]
+ set query [dict remove $query $opt]
+ } else {
+ set replace {}
+ }
+ set input [string map [list %$name% $replace] $input]
+ }
+ return $input
+}
+
+# describe --
+#
+# print a description of defined api calls
+#
+# ARGS:
+# name name of an interface previously created with create_interface
+#
+# RETURNS:
+# nothing
+#
+proc ::rest::describe {name} {
+ # replace [set], then run all the procs to get the value of the config var
+ rename ::set ::_set
+ proc ::set {var val} {
+ if {[lindex [info level 0] 1] != "config"} { continue }
+ upvar 2 config c
+ ::_set c([info level -1]) $val
+ return -code return
+ }
+ foreach call [lsort -dictionary [info commands ::${name}::*]] {
+ if {[string match *::_* $call]} { continue }
+ catch {$call}
+ }
+ rename ::set {}
+ rename ::_set ::set
+
+ foreach {name val} [array get config] {
+ puts -nonewline "$name"
+ if {([dict exists $val req_args] && [dict get $val req_args] != "") \
+ || ([dict exists $val opt_args] && [dict get $val opt_args] != "")} {
+ puts -nonewline " <options>"
+ }
+ if {[dict exists $val body] && [dict get $val body] == "required"} {
+ puts -nonewline " <body>"
+ }
+ puts ""
+ if {[dict exists $val description]} {
+ puts "[regsub -all {[\s\n]+} [dict get $val description] { }]"
+ }
+ if {[dict exists $val callback]} {
+ puts "Async callback: [dict get $val callback]"
+ }
+ puts " Required arguments:"
+ if {[dict exists $val req_args]} {
+ foreach x [dict get $val req_args] {
+ puts " -[format %-12s [string trimright $x :]] <value>"
+ }
+ } else {
+ puts " none"
+ }
+ puts " Optional arguments:"
+ if {[dict exists $val opt_args]} {
+ foreach x [dict get $val opt_args] {
+ if {![string match *:* $x]} {
+ puts " $x"
+ } else {
+ set x [split $x :]
+ if {[lindex $x 1] == ""} {
+ puts " -[format %-12s [lindex $x 0]] <value>"
+ } else {
+ puts " -[format %-12s [lindex $x 0]] <value> default \"[lindex $x 1]\""
+ }
+ }
+ }
+ } else {
+ puts " none"
+ }
+ puts ""
+ }
+}
+
+# _check_result --
+#
+# checks http returned data against user supplied conditions
+#
+# ARGS:
+# result name of the array containing command definitions
+# ok an expression which if it returns false causes an error
+# err an expression which if it returns true causes an error
+#
+# EFFECTS:
+# throws an error if the expression evaluations indicate an error
+#
+# RETURNS:
+# nothing
+#
+proc ::rest::_check_result {result ok err} {
+ if {$err != "" && ![catch {expr $err} out] && [expr {$out}]} {
+ return -code error [list ERR $result "triggered error condition" $err $out]
+ }
+ if {$ok == "" || (![catch {expr $ok} out] && [expr {$out}])} {
+ return -code ok
+ }
+ return -code error [list ERR $result "ok expression failed or returned false" $ok $out]
+}
+
+# format_auto --
+#
+# the default data formatter. tries to detect the data type and dispatch
+# to a specific handler
+#
+# ARGS:
+# data data returned by an http call
+#
+# RETURNS:
+# data, possibly transformed in a representation specific manner
+#
+proc ::rest::format_auto {data} {
+ if {[string match {<*} [string trimleft $data]]} {
+ return [format_xml $data]
+ }
+ if {[string match \{* $data] || [regexp {":\s*[\{\[]} $data]} {
+ return [format_json $data]
+ }
+ return $data
+}
+
+proc ::rest::format_raw {data} {
+ return $data
+}
+
+proc ::rest::format_discard {data} {
+ return -code ok
+}
+
+proc ::rest::format_json {data} {
+ #if {[regexp -nocase {^[a-z_.]+ *= *(.*)} $data -> json]} {
+ # set data $json
+ #}
+ return [json::json2dict $data]
+}
+
+proc ::rest::format_xml {data} {
+ set d [[dom parse $data] documentElement]
+ set data [$d asList]
+ if {[lindex $data 0] == "rss"} {
+ set data [format_rss $data]
+ }
+ return $data
+}
+
+proc ::rest::format_rss {data} {
+ set data [lindex $data 2 0 2]
+ set out {}
+ set channel {}
+ foreach x $data {
+ if {[lindex $x 0] != "item"} {
+ lappend channel [lindex $x 0] \
+ [linsert [lindex $x 1] end content [lindex $x 2 0 1]]
+ } else {
+ set tmp {}
+ foreach item [lindex $x 2] {
+ lappend tmp [lindex $item 0] \
+ [linsert [lindex $item 1] end content [lindex $item 2 0 1]]
+ }
+ lappend out item $tmp
+ }
+ }
+ return [linsert $out 0 channel $channel]
+}
+
+proc ::rest::format_tdom {data} {
+ return [[dom parse $data] documentElement]
+}
diff --git a/tcllib/modules/rest/twitter b/tcllib/modules/rest/twitter
new file mode 100644
index 0000000..c10597f
--- /dev/null
+++ b/tcllib/modules/rest/twitter
@@ -0,0 +1,69 @@
+# documentation: http://apiwiki.twitter.com/REST+API+Documentation
+
+package require rest
+
+set twitter(public_timeline) {
+ url http://twitter.com/statuses/public_timeline.json
+}
+
+set twitter(friends_timeline) {
+ url http://twitter.com/statuses/friends_timeline.json
+ auth basic
+ opt_args { since: since_id: count: page: }
+}
+
+set twitter(user_timeline) {
+ url http://twitter.com/statuses/user_timeline.json
+ auth basic
+ opt_args { id: since: since_id: count: page: }
+}
+
+set twitter(show_status) {
+ url http://twitter.com/statuses/show/%id%.json
+ auth basic
+}
+
+set twitter(update) {
+ url http://twitter.com/statuses/update.json
+ auth basic
+ method post
+ req_args { status: }
+ opt_args { in_reply_to_status_id: }
+}
+
+set twitter(replies) {
+ url http://twitter.com/statuses/replies.json
+ auth basic
+ opt_args { since: since_id: page: }
+}
+
+set twitter(destroy) {
+ url http://twitter.com/statuses/destroy/%id%.json
+ auth basic
+ method post
+}
+
+set twitter(friends) {
+ url http://twitter.com/statuses/friends.json
+ auth basic
+ opt_args { id: page: lite: since: }
+}
+
+set twitter(followers) {
+ url http://twitter.com/statuses/followers.json
+ auth basic
+ opt_args { id: page: lite: }
+}
+
+set twitter(featured) {
+ url http://twitter.com/statuses/featured.json
+ auth basic
+}
+
+set twitter(show_user) {
+ url http://twitter.com/users/show/%id%.json
+ auth basic
+ opt_args { email: }
+}
+
+rest::create_interface twitter
diff --git a/tcllib/modules/rest/yboss b/tcllib/modules/rest/yboss
new file mode 100644
index 0000000..b2d39cb
--- /dev/null
+++ b/tcllib/modules/rest/yboss
@@ -0,0 +1,36 @@
+# documentation: http://developer.yahoo.com/search/boss/boss_guide/
+
+package require rest
+
+set yboss(web) {
+ url http://boss.yahooapis.com/ysearch/web/v1/%query%
+ req_args { appid: }
+ opt_args { start: count: lang: region: sites: filter: type: }
+ post_transform { return [dict get $result ysearchresponse] }
+ check_result { {[dict get $result responsecode] == "200"} {} }
+}
+
+set yboss(news) {
+ url http://boss.yahooapis.com/ysearch/news/v1/%query%
+ req_args { appid: }
+ opt_args { start: count: lang: region: sites: age: }
+ post_transform { return [dict get $result ysearchresponse] }
+ check_result { {[dict $result responsecode] == "200"} {} }
+}
+
+set yboss(images) {
+ url http://boss.yahooapis.com/ysearch/images/v1/%query%
+ req_args { appid: }
+ opt_args { start: count: lang: region: sites: filter: dimensions: refererurl: url: }
+ post_transform { return [dict get $result ysearchresponse] }
+ check_result { {[dict $result responsecode] == "200"} {} }
+}
+
+set yboss(spelling) {
+ url http://boss.yahooapis.com/ysearch/spelling/v1/%query%
+ req_args { appid: }
+ post_transform { return [dict get $result ysearchresponse] }
+ check_result { {[dict $result responsecode] == "200"} {} }
+}
+
+rest::create_interface yboss
diff --git a/tcllib/modules/rest/yweather b/tcllib/modules/rest/yweather
new file mode 100644
index 0000000..95e47ba
--- /dev/null
+++ b/tcllib/modules/rest/yweather
@@ -0,0 +1,19 @@
+package require rest
+
+set yweather(forecast) {
+ url http://weather.yahooapis.com/forecastrss
+ req_args { p: }
+ opt_args { u: }
+}
+
+set yweather(forecast_async) {
+ copy forecast
+ callback ::yweather::callback
+}
+
+rest::create_interface yweather
+
+proc yweather::callback {call status result} {
+ puts "callback from $call $status"
+ puts $result
+}