├── .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/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, "
%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 | --------------------------------------------------------------------------------