Rivet HTTP REST service example

George Petasis: This page contains a small example of how an HTTP REST service can be implemented with Rivet. I don't know if this is the best way to implement such a service, but it works for me (Although my actual implementation uses TclOO). The reason I have created this page, is that the wiki or Rivet site have no examples on REST service implementations.

Code:

package require json
package require json::write

fconfigure stdout -encoding utf-8

##
## Helper functions, for returning results...
##
proc returnJSON {json} {
  fconfigure stdout -encoding utf-8
  ::rivet::headers numeric 200
  ::rivet::headers type {application/json; charset=utf-8}
  ::rivet::headers add Content-Length [string bytelength $json]
  puts $json
};# returnJSON 

proc returnREDIRECT {{id {}}} {
  fconfigure stdout -encoding utf-8
  ::rivet::headers numeric 303
  ## Create a URI for reading the object with this id...
  set scheme [::rivet::env REQUEST_SCHEME]
  set host   [::rivet::env HTTP_HOST]
  set uri    [::rivet::env DOCUMENT_URI]
  if {$id eq ""} {
    ::rivet::headers add Location ${scheme}://${host}/${uri}
  } else {
    ::rivet::headers add Location ${scheme}://${host}/${uri}/$id
  }
  ::rivet::no_body
};# returnREDIRECT

##
## Service methods: these methods actually implement the service operations...
##
namespace eval service {
  proc index {} {
    ::json::write array \
       [read 0] \
       [read 1] \
       [read 2]
  };# index
  
  proc create {object} {
    return id
  };# create

  proc read {id} {
    ::json::write object id   [::json::write string $id] \
                         type [::json::write string sample_object]
  };# read

  proc update {id object} {
    puts $object
  };# update

  proc delete {id} {
  };# delete
}

##
## Get the request method...
##
set method [::rivet::env REQUEST_METHOD]
set path   [::rivet::env PATH_INFO]

##
## The dispatcher, which examines the uri, and calls the appropriate procedure.
##
switch -exact $path {
  {} - / {
    ## Path is either empty, or "/":
    switch -exact $method {
      GET {
        ## index
        # -------------------------------------------------------
        # method: GET
        # path:   /
        # returns: a list of all objects
        returnJSON [service::index]
      }
      POST {
        ## create
        # -------------------------------------------------------
        # method: POST
        # path:   /
        # receives: an object, sent with Content-Type: application/json
        # returns: 303 SEE OTHER redirect to the appropriate read endpoint
        returnREDIRECT [service::create [::rivet::raw_post]]
      }
      default {
        ## Invalid method/post combination. Return an error
        ::rivet::headers type {text/plain; charset=utf-8}
        ::rivet::headers numeric 500 ;# HTTP 500: internal server error
        if {$path eq ""} {set path /}
        puts "invalid method \"$method\" for endpoint \"$path\":"
        puts " valid methods are:"
        puts "    GET:  lists all objects"
        puts "    POST: creates a new object"
        ::rivet::abort_page
      }
    }
  }
  default {
    ## Path is: /<object id>
    switch -glob $path {
      /*/     {set id [string range $path 1 end-1]}
      default {set id [string range $path 1 end]}
    }
    switch -exact $method {
      GET {
        ## read
        # -------------------------------------------------------
        # method: GET
        # path:   /<id>
        # returns: an object
        returnJSON [service::read $id]
      }
      PUT {
        ## update
        # -------------------------------------------------------
        # method: PUT
        # path:   /<id>
        # receives: a (partial) object, sent with Content-Type: application/json
        # returns: 303 SEE OTHER redirect to the appropriate read endpoint
        service::update $id [::rivet::raw_post]
        returnREDIRECT ;# No need to specify an id, the endpoint is the same
      }
      DELETE {
        ## delete
        # -------------------------------------------------------
        # method: DELETE
        # path:   /<id>
        # returns: 204 NO CONTENT, and -- obviously -- no content
        service::delete $id
        ::rivet::headers numeric 204
        ::rivet::no_body
      }
      default {
        ## Invalid method/post combination. Return an error
        headers type {text/plain; charset=utf-8}
        headers numeric 500 ;# HTTP 500: internal server error
        if {$path eq ""} {set path /}
        puts "invalid method \"$method\" for endpoint \"$path\":"
        puts " valid methods are:"
        puts "    GET:    returns the object with id: \"$id\""
        puts "    PUT:    updates the object with id: \"$id\""
        puts "    DELETE: deletes the object with id: \"$id\""
        ::rivet::abort_page
      }
    }
  }
}