diff options
author | William Joye <wjoye@cfa.harvard.edu> | 2016-10-27 19:39:39 (GMT) |
---|---|---|
committer | William Joye <wjoye@cfa.harvard.edu> | 2016-10-27 19:39:39 (GMT) |
commit | ea28451286d3ea4a772fa174483f9a7a66bb1ab3 (patch) | |
tree | 6ee9d8a7848333a7ceeee3b13d492e40225f8b86 /tcllib/modules/rest | |
parent | b5ca09bae0d6a1edce939eea03594dd56383f2c8 (diff) | |
parent | 7c621da28f07e449ad90c387344f07a453927569 (diff) | |
download | blt-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/ChangeLog | 75 | ||||
-rw-r--r-- | tcllib/modules/rest/bitly | 34 | ||||
-rw-r--r-- | tcllib/modules/rest/couchdb | 56 | ||||
-rw-r--r-- | tcllib/modules/rest/delicious | 131 | ||||
-rw-r--r-- | tcllib/modules/rest/facebook | 93 | ||||
-rw-r--r-- | tcllib/modules/rest/flickr | 292 | ||||
-rw-r--r-- | tcllib/modules/rest/gcal | 102 | ||||
-rw-r--r-- | tcllib/modules/rest/gdocs | 87 | ||||
-rw-r--r-- | tcllib/modules/rest/pkgIndex.tcl | 2 | ||||
-rw-r--r-- | tcllib/modules/rest/rest.man | 538 | ||||
-rw-r--r-- | tcllib/modules/rest/rest.tcl | 829 | ||||
-rw-r--r-- | tcllib/modules/rest/twitter | 69 | ||||
-rw-r--r-- | tcllib/modules/rest/yboss | 36 | ||||
-rw-r--r-- | tcllib/modules/rest/yweather | 19 |
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 +} |