Skip to content

Commit 590b9e0

Browse files
committed
Merge pull request #180 from manyminds/dynamic_baseurl_handling
Two new interfaces for baseurl resolving
2 parents 731d3e6 + f65871d commit 590b9e0

File tree

10 files changed

+278
-30
lines changed

10 files changed

+278
-30
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ go get github.com/manyminds/api2go/jsonapi
3838
- [Fetching related IDs](#fetching-related-ids)
3939
- [Fetching related resources](#fetching-related-resources)
4040
- [Using middleware](#using-middleware)
41+
- [Dynamic URL Handling](#dynamic-url-handling)
4142
- [Tests](#tests)
4243

4344
## Examples
@@ -567,6 +568,32 @@ registered with `func (api *API) UseMiddleware(middleware ...HandlerFunc)`. You
567568
that will be executed in order before any other api2go routes. Use this to set up database connections, user authentication
568569
and so on.
569570

571+
### Dynamic URL handling
572+
If you have different TLDs for one api, or want to use different domains in development and production, you can implement a custom
573+
URLResolver in api2go.
574+
575+
There is a simple interface, which can be used if you get TLD information from the database, the server environment, or anything else
576+
that's not request dependant:
577+
```go
578+
type URLResolver interface {
579+
GetBaseURL() string
580+
}
581+
```
582+
And a more complex one that also gets request information:
583+
```go
584+
type RequestAwareURLResolver interface {
585+
URLResolver
586+
SetRequest(http.Request)
587+
}
588+
```
589+
590+
For most use cases we provide a CallbackResolver which works on a per request basis and may fill
591+
your basic needs. This is particulary useful if you are using an nginx proxy which sets `X-Forwarded-For` headers.
592+
593+
```go
594+
resolver := NewCallbackResolver(func(r http.Request) string{})
595+
api := NewApiWithMarshalling("v1", resolver, marshalers)
596+
```
570597
## Tests
571598

572599
```sh

api.go

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ func (r response) StatusCode() int {
3737
}
3838

3939
type information struct {
40-
prefix string
41-
baseURL string
40+
prefix string
41+
resolver URLResolver
4242
}
4343

4444
func (i information) GetBaseURL() string {
45-
return i.baseURL
45+
return i.resolver.GetBaseURL()
4646
}
4747

4848
func (i information) GetPrefix() string {
@@ -235,6 +235,18 @@ func (api *API) addResource(prototype jsonapi.MarshalIdentifier, source CRUD, ma
235235
marshalers: marshalers,
236236
}
237237

238+
requestInfo := func(r *http.Request) *information {
239+
var info *information
240+
if resolver, ok := api.info.resolver.(RequestAwareURLResolver); ok {
241+
resolver.SetRequest(*r)
242+
info = &information{prefix: api.prefix, resolver: resolver}
243+
} else {
244+
info = &api.info
245+
}
246+
247+
return info
248+
}
249+
238250
api.router.Handle("OPTIONS", api.prefix+name, func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
239251
c := api.contextPool.Get().(APIContexter)
240252
c.Reset()
@@ -254,21 +266,24 @@ func (api *API) addResource(prototype jsonapi.MarshalIdentifier, source CRUD, ma
254266
})
255267

256268
api.router.GET(api.prefix+name, func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
269+
info := requestInfo(r)
257270
c := api.contextPool.Get().(APIContexter)
258271
c.Reset()
259272
api.middlewareChain(c, w, r)
260-
err := res.handleIndex(c, w, r, api.info)
273+
274+
err := res.handleIndex(c, w, r, *info)
261275
api.contextPool.Put(c)
262276
if err != nil {
263277
handleError(err, w, r, marshalers)
264278
}
265279
})
266280

267281
api.router.GET(api.prefix+name+"/:id", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
282+
info := requestInfo(r)
268283
c := api.contextPool.Get().(APIContexter)
269284
c.Reset()
270285
api.middlewareChain(c, w, r)
271-
err := res.handleRead(c, w, r, ps, api.info)
286+
err := res.handleRead(c, w, r, ps, *info)
272287
api.contextPool.Put(c)
273288
if err != nil {
274289
handleError(err, w, r, marshalers)
@@ -282,10 +297,11 @@ func (api *API) addResource(prototype jsonapi.MarshalIdentifier, source CRUD, ma
282297
for _, relation := range relations {
283298
api.router.GET(api.prefix+name+"/:id/relationships/"+relation.Name, func(relation jsonapi.Reference) httprouter.Handle {
284299
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
300+
info := requestInfo(r)
285301
c := api.contextPool.Get().(APIContexter)
286302
c.Reset()
287303
api.middlewareChain(c, w, r)
288-
err := res.handleReadRelation(c, w, r, ps, api.info, relation)
304+
err := res.handleReadRelation(c, w, r, ps, *info, relation)
289305
api.contextPool.Put(c)
290306
if err != nil {
291307
handleError(err, w, r, marshalers)
@@ -295,10 +311,11 @@ func (api *API) addResource(prototype jsonapi.MarshalIdentifier, source CRUD, ma
295311

296312
api.router.GET(api.prefix+name+"/:id/"+relation.Name, func(relation jsonapi.Reference) httprouter.Handle {
297313
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
314+
info := requestInfo(r)
298315
c := api.contextPool.Get().(APIContexter)
299316
c.Reset()
300317
api.middlewareChain(c, w, r)
301-
err := res.handleLinked(c, api, w, r, ps, relation, api.info)
318+
err := res.handleLinked(c, api, w, r, ps, relation, *info)
302319
api.contextPool.Put(c)
303320
if err != nil {
304321
handleError(err, w, r, marshalers)
@@ -351,10 +368,11 @@ func (api *API) addResource(prototype jsonapi.MarshalIdentifier, source CRUD, ma
351368
}
352369

353370
api.router.POST(api.prefix+name, func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
371+
info := requestInfo(r)
354372
c := api.contextPool.Get().(APIContexter)
355373
c.Reset()
356374
api.middlewareChain(c, w, r)
357-
err := res.handleCreate(c, w, r, api.prefix, api.info)
375+
err := res.handleCreate(c, w, r, api.prefix, *info)
358376
api.contextPool.Put(c)
359377
if err != nil {
360378
handleError(err, w, r, marshalers)

api_interfaces.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package api2go
22

3+
import "net/http"
4+
35
// The CRUD interface MUST be implemented in order to use the api2go api.
46
type CRUD interface {
57
// FindOne returns an object by its ID
@@ -54,6 +56,29 @@ type FindAll interface {
5456
FindAll(req Request) (Responder, error)
5557
}
5658

59+
//URLResolver allows you to implement a static
60+
//way to return a baseURL for all incoming
61+
//requests for one api2go instance.
62+
type URLResolver interface {
63+
GetBaseURL() string
64+
}
65+
66+
//RequestAwareURLResolver allows you to dynamically change
67+
//generated urls.
68+
//
69+
//This is particulary useful if you have the same
70+
//API answering to multiple domains, or subdomains
71+
//e.g customer[1,2,3,4].yourapi.example.com
72+
//
73+
//SetRequest will always be called prior to
74+
//the GetBaseURL() from `URLResolver` so you
75+
//have to change the result value based on the last
76+
//request.
77+
type RequestAwareURLResolver interface {
78+
URLResolver
79+
SetRequest(http.Request)
80+
}
81+
5782
// The Responder interface is used by all Resource Methods as a container for the Response.
5883
// Metadata is additional Metadata. You can put anything you like into it, see jsonapi spec.
5984
// Result returns the actual payload. For FindOne, put only one entry in it.

api_public.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,21 @@ func (api *API) SetRedirectTrailingSlash(enabled bool) {
7373
api.router.RedirectTrailingSlash = enabled
7474
}
7575

76-
// NewAPIWithMarshalers does the same as NewAPIWithBaseURL with the addition
76+
//NewAPIWithMarshalers is DEPRECATED
77+
//use NewApiWithMarshalling instead
78+
func NewAPIWithMarshalers(prefix string, baseURL string, marshalers map[string]ContentMarshaler) *API {
79+
staticResolver := newStaticResolver(baseURL)
80+
return NewAPIWithMarshalling(prefix, staticResolver, marshalers)
81+
}
82+
83+
// NewAPIWithMarshalling does the same as NewAPIWithBaseURL with the addition
7784
// of a set of marshalers that provide a way to interact with clients that
7885
// use a serialization format other than JSON. The marshalers map is indexed
7986
// by the MIME content type to use for a given request-response pair. If the
8087
// client provides an Accept header the server will respond using the client's
8188
// preferred content type, otherwise it will respond using whatever content
8289
// type the client provided in its Content-Type request header.
83-
func NewAPIWithMarshalers(prefix string, baseURL string, marshalers map[string]ContentMarshaler) *API {
90+
func NewAPIWithMarshalling(prefix string, resolver URLResolver, marshalers map[string]ContentMarshaler) *API {
8491
if len(marshalers) == 0 {
8592
panic("marshaler map must not be empty")
8693
}
@@ -96,7 +103,7 @@ func NewAPIWithMarshalers(prefix string, baseURL string, marshalers map[string]C
96103
router := httprouter.New()
97104
router.MethodNotAllowed = notAllowedHandler{marshalers: marshalers}
98105

99-
info := information{prefix: prefix, baseURL: baseURL}
106+
info := information{prefix: prefix, resolver: resolver}
100107

101108
api := &API{
102109
router: router,

api_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@ import (
1616
"gopkg.in/guregu/null.v2"
1717
)
1818

19+
type requestURLResolver struct {
20+
r http.Request
21+
calls int
22+
}
23+
24+
func (m requestURLResolver) GetBaseURL() string {
25+
if uri := m.r.Header.Get("REQUEST_URI"); uri != "" {
26+
return uri
27+
}
28+
return "https://example.com"
29+
}
30+
31+
func (m *requestURLResolver) SetRequest(r http.Request) {
32+
m.r = r
33+
}
34+
1935
type invalid string
2036

2137
func (i invalid) GetID() string {
@@ -1516,4 +1532,59 @@ var _ = Describe("RestHandler", func() {
15161532
})
15171533

15181534
})
1535+
1536+
Context("dynamic baseurl handling", func() {
1537+
var (
1538+
api *API
1539+
rec *httptest.ResponseRecorder
1540+
source *fixtureSource
1541+
)
1542+
1543+
BeforeEach(func() {
1544+
source = &fixtureSource{map[string]*Post{
1545+
"1": {ID: "1", Title: "Hello, World!"},
1546+
}, false}
1547+
1548+
marshalers := map[string]ContentMarshaler{
1549+
`application/vnd.api+json`: JSONContentMarshaler{},
1550+
}
1551+
1552+
api = NewAPIWithMarshalling("/secret/", &requestURLResolver{}, marshalers)
1553+
api.AddResource(Post{}, source)
1554+
rec = httptest.NewRecorder()
1555+
})
1556+
1557+
It("should change dependening on request header in FindAll", func() {
1558+
firstURI := "https://god-mode.example.com"
1559+
secondURI := "https://top-secret.example.com"
1560+
req, err := http.NewRequest("GET", "/secret/posts", nil)
1561+
req.Header.Set("REQUEST_URI", firstURI)
1562+
Expect(err).To(BeNil())
1563+
api.Handler().ServeHTTP(rec, req)
1564+
Expect(rec.Code).To(Equal(http.StatusOK))
1565+
Expect(err).ToNot(HaveOccurred())
1566+
Expect(rec.Body.Bytes()).To(ContainSubstring(firstURI))
1567+
Expect(rec.Body.Bytes()).ToNot(ContainSubstring(secondURI))
1568+
rec = httptest.NewRecorder()
1569+
req2, err := http.NewRequest("GET", "/secret/posts", nil)
1570+
req2.Header.Set("REQUEST_URI", secondURI)
1571+
Expect(err).To(BeNil())
1572+
api.Handler().ServeHTTP(rec, req2)
1573+
Expect(rec.Code).To(Equal(http.StatusOK))
1574+
Expect(err).ToNot(HaveOccurred())
1575+
Expect(rec.Body.Bytes()).To(ContainSubstring(secondURI))
1576+
Expect(rec.Body.Bytes()).ToNot(ContainSubstring(firstURI))
1577+
})
1578+
1579+
It("should change dependening on request header in FindOne", func() {
1580+
expected := "https://god-mode.example.com"
1581+
req, err := http.NewRequest("GET", "/secret/posts/1", nil)
1582+
req.Header.Set("REQUEST_URI", expected)
1583+
Expect(err).To(BeNil())
1584+
api.Handler().ServeHTTP(rec, req)
1585+
Expect(rec.Code).To(Equal(http.StatusOK))
1586+
Expect(err).ToNot(HaveOccurred())
1587+
Expect(rec.Body.Bytes()).To(ContainSubstring(expected))
1588+
})
1589+
})
15191590
})

examples/crud_example.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Package examples shows how to implement a basic CRUD for two data structures with the api2go server functionality.
33
To play with this example server you can run some of the following curl requests
44
5+
In order to demonstrate dynamic baseurl handling for requests, apply the --header="REQUEST_URI:https://www.your.domain.example.com" parameter to any of the commands.
6+
57
Create a new user:
68
curl -X POST http://localhost:31415/v0/users -d '{"data" : [{"type" : "users" , "attributes": {"user-name" : "marvin"}}]}'
79
@@ -47,6 +49,7 @@ import (
4749
"github.com/julienschmidt/httprouter"
4850
"github.com/manyminds/api2go"
4951
"github.com/manyminds/api2go/examples/model"
52+
"github.com/manyminds/api2go/examples/resolver"
5053
"github.com/manyminds/api2go/examples/resource"
5154
"github.com/manyminds/api2go/examples/storage"
5255
)
@@ -75,17 +78,19 @@ func main() {
7578
"application/vnd.api+json": PrettyJSONContentMarshaler{},
7679
}
7780

78-
api := api2go.NewAPIWithMarshalers("v0", "http://localhost:31415", marshalers)
81+
port := 31415
82+
api := api2go.NewAPIWithMarshalling("v0", &resolver.RequestURL{Port: port}, marshalers)
7983
userStorage := storage.NewUserStorage()
8084
chocStorage := storage.NewChocolateStorage()
8185
api.AddResource(model.User{}, resource.UserResource{ChocStorage: chocStorage, UserStorage: userStorage})
8286
api.AddResource(model.Chocolate{}, resource.ChocolateResource{ChocStorage: chocStorage, UserStorage: userStorage})
8387

84-
fmt.Println("Listening on :31415")
88+
fmt.Printf("Listening on :%d", port)
8589
handler := api.Handler().(*httprouter.Router)
8690
// It is also possible to get the instance of julienschmidt/httprouter and add more custom routes!
8791
handler.GET("/hello-world", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
8892
fmt.Fprint(w, "Hello World!\n")
8993
})
90-
http.ListenAndServe(":31415", handler)
94+
95+
http.ListenAndServe(fmt.Sprintf(":%d", port), handler)
9196
}

examples/resolver/resolver.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package resolver
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
)
7+
8+
//RequestURL simply returns
9+
//the request url from REQUEST_URI header
10+
//this should not be done in production applications
11+
type RequestURL struct {
12+
r http.Request
13+
Port int
14+
}
15+
16+
//SetRequest to implement `RequestAwareResolverInterface`
17+
func (m *RequestURL) SetRequest(r http.Request) {
18+
m.r = r
19+
}
20+
21+
//GetBaseURL implements `URLResolver` interface
22+
func (m RequestURL) GetBaseURL() string {
23+
if uri := m.r.Header.Get("REQUEST_URI"); uri != "" {
24+
return uri
25+
}
26+
27+
return fmt.Sprintf("https://localhost:%d", m.Port)
28+
}

0 commit comments

Comments
 (0)