├── .gitignore ├── LICENSE ├── README.md ├── client.go ├── doc.go ├── examples └── snips │ ├── .gitignore │ ├── README │ ├── collection.go │ ├── rest.go │ └── snips.go └── server.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.6 2 | *.8 3 | *.o 4 | *.so 5 | *.out 6 | *.go~ 7 | *.cgo?.* 8 | _cgo_* 9 | _obj 10 | _test 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Nathan Kerr 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A RESTful HTTP client and server. 2 | 3 | goci: [![](http://goci.me/project/image/github.com/nathankerr/rest)](http://goci.me/project/github.com/nathankerr/rest) 4 | 5 | docs: http://go.pkgdoc.org/github.com/nathankerr/rest 6 | 7 | Install by running: 8 | 9 | go get github.com/nathankerr/rest 10 | 11 | Checkout examples/snips/snips.go for a simple client and server example 12 | 13 | rest uses the standard http package by adding resource routes. Add 14 | a new route by: 15 | 16 | rest.Resource("resourcepath", resourcevariable) 17 | 18 | and then use http as normal. 19 | 20 | A resource is an object that may have any of the following methods which 21 | respond to the specified HTTP requests: 22 | 23 | GET /resource/ => Index(http.ResponseWriter, *http.Request) 24 | GET /resource/id => Find(http.ResponseWriter, id string, *http.Request) 25 | POST /resource/ => Create(http.ResponseWriter, *http.Request) 26 | PUT /resource/id => Update(http.ResponseWriter, id string, *http.Request) 27 | DELETE /resource/id => Delete(http.ResponseWriter, id string, *http.Request) 28 | OPTIONS /resource/ => Options(http.ResponseWriter, id string, *http.Request) 29 | OPTIONS /resource/id => Options(http.ResponseWriter, id string, *http.Request) 30 | 31 | The server will then route HTTP requests to the appropriate method call. 32 | 33 | The snips example provides a full example of both a client and server. -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | "net/http" 8 | "net/http/httputil" 9 | "net/url" 10 | ) 11 | 12 | type Client struct { 13 | conn *httputil.ClientConn 14 | resource *url.URL 15 | } 16 | 17 | // Creates a client for the specified resource. 18 | // 19 | // The resource is the url to the base of the resource (i.e., 20 | // http://127.0.0.1:3000/snips/) 21 | func NewClient(resource string) (*Client, error) { 22 | var client = new(Client) 23 | var err error 24 | 25 | // setup host 26 | if client.resource, err = url.Parse(resource); err != nil { 27 | return nil, err 28 | } 29 | 30 | // Setup conn 31 | var tcpConn net.Conn 32 | if tcpConn, err = net.Dial("tcp", client.resource.Host); err != nil { 33 | return nil, err 34 | } 35 | client.conn = httputil.NewClientConn(tcpConn, nil) 36 | 37 | return client, nil 38 | } 39 | 40 | // Closes the clients connection 41 | func (client *Client) Close() { 42 | client.conn.Close() 43 | } 44 | 45 | // General Request method used by the specialized request methods to create a request 46 | func (client *Client) newRequest(method string, id string) (*http.Request, error) { 47 | request := new(http.Request) 48 | var err error 49 | 50 | request.ProtoMajor = 1 51 | request.ProtoMinor = 1 52 | request.TransferEncoding = []string{"chunked"} 53 | 54 | request.Method = method 55 | 56 | // Generate Resource-URI and parse it 57 | uri := client.resource.String() + id 58 | if request.URL, err = url.Parse(uri); err != nil { 59 | return nil, err 60 | } 61 | 62 | return request, nil 63 | } 64 | 65 | // Send a request 66 | func (client *Client) Request(request *http.Request) (*http.Response, error) { 67 | var err error 68 | var response *http.Response 69 | 70 | // Send the request 71 | if err = client.conn.Write(request); err != nil { 72 | return nil, err 73 | } 74 | 75 | // Read the response 76 | if response, err = client.conn.Read(request); err != nil { 77 | return nil, err 78 | } 79 | 80 | return response, nil 81 | } 82 | 83 | // GET /resource/ 84 | func (client *Client) Index() (*http.Response, error) { 85 | var request *http.Request 86 | var err error 87 | 88 | if request, err = client.newRequest("GET", ""); err != nil { 89 | return nil, err 90 | } 91 | 92 | return client.Request(request) 93 | } 94 | 95 | // GET /resource/id 96 | func (client *Client) Find(id string) (*http.Response, error) { 97 | var request *http.Request 98 | var err error 99 | 100 | if request, err = client.newRequest("GET", id); err != nil { 101 | return nil, err 102 | } 103 | 104 | return client.Request(request) 105 | } 106 | 107 | type nopCloser struct { 108 | io.Reader 109 | } 110 | 111 | func (nopCloser) Close() error { 112 | return nil 113 | } 114 | 115 | // POST /resource 116 | func (client *Client) Create(body string) (*http.Response, error) { 117 | var request *http.Request 118 | var err error 119 | 120 | if request, err = client.newRequest("POST", ""); err != nil { 121 | return nil, err 122 | } 123 | 124 | request.Body = nopCloser{bytes.NewBufferString(body)} 125 | 126 | return client.Request(request) 127 | } 128 | 129 | // PUT /resource/id 130 | func (client *Client) Update(id string, body string) (*http.Response, error) { 131 | var request *http.Request 132 | var err error 133 | if request, err = client.newRequest("PUT", id); err != nil { 134 | return nil, err 135 | } 136 | 137 | request.Body = nopCloser{bytes.NewBufferString(body)} 138 | 139 | return client.Request(request) 140 | } 141 | 142 | // Parse a response-Location-URI to get the ID of the worked-on snip 143 | func (client *Client) IdFromURL(urlString string) (string, error) { 144 | var uri *url.URL 145 | var err error 146 | if uri, err = url.Parse(urlString); err != nil { 147 | return "", err 148 | } 149 | 150 | return string(uri.Path[len(client.resource.Path):]), nil 151 | } 152 | 153 | // DELETE /resource/id 154 | func (client *Client) Delete(id string) (*http.Response, error) { 155 | var request *http.Request 156 | var err error 157 | if request, err = client.newRequest("DELETE", id); err != nil { 158 | return nil, err 159 | } 160 | 161 | return client.Request(request) 162 | } 163 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | A RESTful HTTP client and server. 3 | 4 | rest uses the standard http package by adding resource routes. Add 5 | a new route by: 6 | 7 | rest.Resource("resourcepath", resourcevariable) 8 | 9 | and then use http as normal. 10 | 11 | A resource is an object that may have any of the following methods which 12 | respond to the specified HTTP requests: 13 | 14 | GET /resource/ => Index(http.ResponseWriter) 15 | GET /resource/id => Find(http.ResponseWriter, id string) 16 | POST /resource/ => Create(http.ResponseWriter, *http.Request) 17 | PUT /resource/id => Update(http.ResponseWriter, id string, *http.Request) 18 | DELETE /resource/id => Delete(http.ResponseWriter, id string) 19 | OPTIONS /resource/ => Options(http.ResponseWriter, id string) 20 | OPTIONS /resource/id => Options(http.ResponseWriter, id string) 21 | 22 | The server will then route HTTP requests to the appropriate method call. 23 | 24 | The snips example provides a full example of both a client and server. 25 | */ 26 | package rest 27 | -------------------------------------------------------------------------------- /examples/snips/.gitignore: -------------------------------------------------------------------------------- 1 | snips 2 | -------------------------------------------------------------------------------- /examples/snips/README: -------------------------------------------------------------------------------- 1 | A simple server and client. 2 | 3 | Compile with 4 | $ gomake 5 | 6 | Start server with 7 | $ ./snips -server 8 | 9 | Run client with 10 | $ ./snips 11 | 12 | You can also access the server with other tools or your webbrowser (which will allows you to at least Index and Find(Id) by GET requests). 13 | Open http://127.0.0.1:3000/snips/ in your browser. 14 | 15 | -------------------------------------------------------------------------------- /examples/snips/collection.go: -------------------------------------------------------------------------------- 1 | /* 2 | Defines SnipsCollection struct-type and NewSnipsCollection() 3 | It is used by the example REST server for storing and managing snips 4 | It has the methods Add, WithId, All and Remove. 5 | */ 6 | package main 7 | 8 | import ( 9 | "log" 10 | ) 11 | 12 | // Snip definintion 13 | type Snip struct { 14 | Id int 15 | Body string 16 | } 17 | 18 | func NewSnip(id int, body string) *Snip { 19 | log.Println("Creating new Snip:", id, body) 20 | return &Snip{id, body} 21 | } 22 | 23 | // SnipsCollection definition 24 | type SnipsCollection struct { 25 | snips map[int]string 26 | nextId int 27 | } 28 | 29 | func NewSnipsCollection() *SnipsCollection { 30 | log.Println("Creating new SnipsCollection") 31 | return &SnipsCollection{make(map[int]string), 0} 32 | } 33 | 34 | func (c *SnipsCollection) Add(body string) int { 35 | log.Println("Adding Snip:", body) 36 | id := c.nextId 37 | c.nextId++ 38 | 39 | c.snips[id] = body 40 | 41 | return id 42 | } 43 | 44 | func (c *SnipsCollection) WithId(id int) (*Snip, bool) { 45 | log.Println("Finding Snip with id: ", id) 46 | body, ok := c.snips[id] 47 | if !ok { 48 | return nil, false 49 | } 50 | 51 | return &Snip{id, body}, true 52 | } 53 | 54 | func (c *SnipsCollection) All() []*Snip { 55 | log.Println("Finding all Snips") 56 | all := make([]*Snip, len(c.snips)) 57 | 58 | for id, body := range c.snips { 59 | all[id] = &Snip{id, body} 60 | } 61 | 62 | return all 63 | } 64 | 65 | func (c *SnipsCollection) Remove(id int) { 66 | delete(c.snips, id) 67 | } 68 | -------------------------------------------------------------------------------- /examples/snips/rest.go: -------------------------------------------------------------------------------- 1 | /* 2 | Defines Index, Find, Create, Update and Delete methods for SnipsCollection 3 | These will be called on the corresponding REST requests 4 | */ 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "github.com/nathankerr/rest" 10 | "io/ioutil" 11 | "net/http" 12 | "strconv" 13 | ) 14 | 15 | // String used as error message on invalid formatting (request body could not be parsed into a snip) 16 | var formatting = "formatting instructions go here" 17 | 18 | // Get an index of the snips in the collection 19 | func (snips *SnipsCollection) Index(c http.ResponseWriter, r *http.Request) { 20 | for _, snip := range snips.All() { 21 | fmt.Fprintf(c, "%v%v
", snip.Id, snip.Id, snip.Body) 22 | } 23 | } 24 | 25 | // Find a snip from the collection, identified by the ID 26 | func (snips *SnipsCollection) Find(c http.ResponseWriter, idString string, r *http.Request) { 27 | id, err := strconv.Atoi(idString) 28 | if err != nil { 29 | rest.NotFound(c) 30 | return 31 | } 32 | 33 | snip, ok := snips.WithId(id) 34 | if !ok { 35 | rest.NotFound(c) 36 | return 37 | } 38 | 39 | fmt.Fprintf(c, "

Snip %v

%v

", snip.Id, snip.Body) 40 | } 41 | 42 | // Create and add a new snip to the collection 43 | func (snips *SnipsCollection) Create(c http.ResponseWriter, request *http.Request) { 44 | data, err := ioutil.ReadAll(request.Body) 45 | if err != nil { 46 | rest.BadRequest(c, formatting) 47 | return 48 | } 49 | 50 | id := snips.Add(string(data)) 51 | rest.Created(c, fmt.Sprintf("%v%v", request.URL.String(), id)) 52 | } 53 | 54 | // Update a snip identified by an ID with the data sent as request-body 55 | func (snips *SnipsCollection) Update(c http.ResponseWriter, idString string, request *http.Request) { 56 | // Parse ID of type string to int 57 | var id int 58 | var err error 59 | if id, err = strconv.Atoi(idString); err != nil { 60 | // The ID could not be converted from string to int 61 | rest.NotFound(c) 62 | return 63 | } 64 | 65 | // Find the snip with the ID 66 | var snip *Snip 67 | var ok bool 68 | if snip, ok = snips.WithId(id); !ok { 69 | // A snip with the passed ID could not be found in our collection 70 | rest.NotFound(c) 71 | } 72 | 73 | // Get the request-body for data to update the snipped to 74 | var data []byte 75 | if data, err = ioutil.ReadAll(request.Body); err != nil { 76 | // The request body could not be read, thus it was a bad request 77 | rest.BadRequest(c, formatting) 78 | return 79 | } 80 | 81 | // Set the snips body 82 | snip.Body = string(data) 83 | // Respond to indicate successful update 84 | rest.Updated(c, request.URL.String()) 85 | } 86 | 87 | // Delete a snip identified by ID from the collection 88 | func (snips *SnipsCollection) Delete(c http.ResponseWriter, idString string, r *http.Request) { 89 | var id int 90 | var err error 91 | if id, err = strconv.Atoi(idString); err != nil { 92 | rest.NotFound(c) 93 | } 94 | 95 | snips.Remove(id) 96 | rest.NoContent(c) 97 | } 98 | -------------------------------------------------------------------------------- /examples/snips/snips.go: -------------------------------------------------------------------------------- 1 | // Example REST server and client. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "github.com/nathankerr/rest" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | var server = flag.Bool("server", false, "start in server mode") 14 | 15 | func main() { 16 | flag.Parse() 17 | 18 | if *server { 19 | serve() 20 | } else { 21 | client() 22 | } 23 | } 24 | 25 | func serve() { 26 | log.Println("Starting Server") 27 | address := "127.0.0.1:3000" 28 | snips := NewSnipsCollection() 29 | 30 | snips.Add("first post!") 31 | snips.Add("me too") 32 | 33 | rest.Resource("snips", snips) 34 | 35 | if err := http.ListenAndServe(address, nil); err != nil { 36 | log.Fatalln(err) 37 | } 38 | } 39 | 40 | func client() { 41 | log.Println("Starting Client") 42 | var snips *rest.Client 43 | var err error 44 | 45 | if snips, err = rest.NewClient("http://127.0.0.1:3000/snips/"); err != nil { 46 | log.Fatalln(err) 47 | } 48 | 49 | // Create a new snip 50 | var response *http.Response 51 | if response, err = snips.Create("newone"); err != nil { 52 | log.Fatalln(err) 53 | } 54 | log.Println("Sent create request for 'newone'") 55 | // Get the ID for the just created snip by checking the response Location. 56 | var id string 57 | if id, err = snips.IdFromURL(response.Header.Get("Location")); err != nil { 58 | log.Fatalln(err) 59 | } 60 | log.Println("'newone' has been added with id ", id) 61 | 62 | // Update the snip 63 | if response, err = snips.Update(id, "updated"); err != nil { 64 | log.Fatalln(err) 65 | } 66 | log.Println("Sent snip-update request") 67 | 68 | // Get the updated snip 69 | if response, err = snips.Find(id); err != nil { 70 | log.Fatalln(err) 71 | } 72 | 73 | var data []byte 74 | if data, err = ioutil.ReadAll(response.Body); err != nil { 75 | log.Fatalln(err) 76 | } 77 | 78 | fmt.Println("Added and updated snip has been requested. Result:") 79 | fmt.Printf("%v\n", string(data)) 80 | 81 | // Delete the created snip 82 | if response, err = snips.Delete(id); err != nil { 83 | log.Fatalln(err) 84 | } 85 | log.Println("Delete request has been sent") 86 | 87 | } 88 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | var resources = make(map[string]interface{}) 10 | 11 | // Lists all the items in the resource 12 | // GET /resource/ 13 | type index interface { 14 | Index(http.ResponseWriter, *http.Request) 15 | } 16 | 17 | // Creates a new resource item 18 | // POST /resource/ 19 | type create interface { 20 | Create(http.ResponseWriter, *http.Request) 21 | } 22 | 23 | // Views a resource item 24 | // GET /resource/id 25 | type find interface { 26 | Find(http.ResponseWriter, string, *http.Request) 27 | } 28 | 29 | // PUT /resource/id 30 | type update interface { 31 | Update(http.ResponseWriter, string, *http.Request) 32 | } 33 | 34 | // DELETE /resource/id 35 | type delete interface { 36 | Delete(http.ResponseWriter, string, *http.Request) 37 | } 38 | 39 | // Return options to use the service. If string is nil, then it is the base URL 40 | // OPTIONS /resource/id 41 | // OPTIONS /resource/ 42 | type options interface { 43 | Options(http.ResponseWriter, string, *http.Request) 44 | } 45 | 46 | // Generic resource handler 47 | func resourceHandler(c http.ResponseWriter, req *http.Request) { 48 | // Parse request URI to resource URI and (potential) ID 49 | var resourceEnd = strings.Index(req.URL.Path[1:], "/") + 1 50 | var resourceName string 51 | if resourceEnd == -1 { 52 | resourceName = req.URL.Path[1:] 53 | } else { 54 | resourceName = req.URL.Path[1:resourceEnd] 55 | } 56 | var id = req.URL.Path[resourceEnd+1:] 57 | 58 | resource, ok := resources[resourceName] 59 | if !ok { 60 | fmt.Fprintf(c, "resource %s not found\n", resourceName) 61 | } 62 | 63 | if len(id) == 0 { 64 | switch req.Method { 65 | case "GET": 66 | // Index 67 | if resIndex, ok := resource.(index); ok { 68 | resIndex.Index(c, req) 69 | } else { 70 | NotImplemented(c) 71 | } 72 | case "POST": 73 | // Create 74 | if resCreate, ok := resource.(create); ok { 75 | resCreate.Create(c, req) 76 | } else { 77 | NotImplemented(c) 78 | } 79 | case "OPTIONS": 80 | // automatic options listing 81 | if resOptions, ok := resource.(options); ok { 82 | resOptions.Options(c, id, req) 83 | } else { 84 | NotImplemented(c) 85 | } 86 | default: 87 | NotImplemented(c) 88 | } 89 | } else { // ID was passed 90 | switch req.Method { 91 | case "GET": 92 | // Find 93 | if resFind, ok := resource.(find); ok { 94 | resFind.Find(c, id, req) 95 | } else { 96 | NotImplemented(c) 97 | } 98 | case "PUT": 99 | // Update 100 | if resUpdate, ok := resource.(update); ok { 101 | resUpdate.Update(c, id, req) 102 | } else { 103 | NotImplemented(c) 104 | } 105 | case "DELETE": 106 | // Delete 107 | if resDelete, ok := resource.(delete); ok { 108 | resDelete.Delete(c, id, req) 109 | } else { 110 | NotImplemented(c) 111 | } 112 | case "OPTIONS": 113 | // automatic options 114 | if resOptions, ok := resource.(options); ok { 115 | resOptions.Options(c, id, req) 116 | } else { 117 | NotImplemented(c) 118 | } 119 | default: 120 | NotImplemented(c) 121 | } 122 | } 123 | } 124 | 125 | // Add a resource route to http 126 | func Resource(name string, res interface{}) { 127 | resources[name] = res 128 | http.Handle("/"+name+"/", http.HandlerFunc(resourceHandler)) 129 | } 130 | 131 | // Emits a 404 Not Found 132 | func NotFound(c http.ResponseWriter) { 133 | http.Error(c, "404 Not Found", http.StatusNotFound) 134 | } 135 | 136 | // Emits a 501 Not Implemented 137 | func NotImplemented(c http.ResponseWriter) { 138 | http.Error(c, "501 Not Implemented", http.StatusNotImplemented) 139 | } 140 | 141 | // Emits a 201 Created with the URI for the new location 142 | func Created(c http.ResponseWriter, location string) { 143 | c.Header().Set("Location", location) 144 | http.Error(c, "201 Created", http.StatusCreated) 145 | } 146 | 147 | // Emits a 200 OK with a location. Used when after a PUT 148 | func Updated(c http.ResponseWriter, location string) { 149 | c.Header().Set("Location", location) 150 | http.Error(c, "200 OK", http.StatusOK) 151 | } 152 | 153 | // Emits a bad request with the specified instructions 154 | func BadRequest(c http.ResponseWriter, instructions string) { 155 | c.WriteHeader(http.StatusBadRequest) 156 | c.Write([]byte(instructions)) 157 | } 158 | 159 | // Emits a 204 No Content 160 | func NoContent(c http.ResponseWriter) { 161 | http.Error(c, "204 No Content", http.StatusNoContent) 162 | } 163 | --------------------------------------------------------------------------------