Skip to content

Commit 02704db

Browse files
committed
Merge pull request #209 from manyminds/sparse-fieldsets
sparse fieldsets filtering support
2 parents 4b5c8ff + 39eea9c commit 02704db

File tree

2 files changed

+324
-3
lines changed

2 files changed

+324
-3
lines changed

api.go

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http"
99
"net/url"
1010
"reflect"
11+
"regexp"
1112
"strconv"
1213
"strings"
1314

@@ -16,7 +17,12 @@ import (
1617
"github.com/manyminds/api2go/routing"
1718
)
1819

19-
const defaultContentTypHeader = "application/vnd.api+json"
20+
const (
21+
codeInvalidQueryFields = "API2GO_INVALID_FIELD_QUERY_PARAM"
22+
defaultContentTypHeader = "application/vnd.api+json"
23+
)
24+
25+
var queryFieldsRegex = regexp.MustCompile(`^fields\[(\w+)\]$`)
2026

2127
type response struct {
2228
Meta map[string]interface{}
@@ -994,14 +1000,125 @@ func unmarshalRequest(r *http.Request, marshalers map[string]ContentMarshaler) (
9941000

9951001
func marshalResponse(resp interface{}, w http.ResponseWriter, status int, r *http.Request, marshalers map[string]ContentMarshaler) error {
9961002
marshaler, contentType := selectContentMarshaler(r, marshalers)
997-
result, err := marshaler.Marshal(resp)
1003+
filtered, err := filterSparseFields(resp, r)
1004+
if err != nil {
1005+
return err
1006+
}
1007+
result, err := marshaler.Marshal(filtered)
9981008
if err != nil {
9991009
return err
10001010
}
10011011
writeResult(w, result, status, contentType)
10021012
return nil
10031013
}
10041014

1015+
func filterSparseFields(resp interface{}, r *http.Request) (interface{}, error) {
1016+
query := r.URL.Query()
1017+
queryParams := parseQueryFields(&query)
1018+
if len(queryParams) < 1 {
1019+
return resp, nil
1020+
}
1021+
1022+
if content, ok := resp.(map[string]interface{}); ok {
1023+
wrongFields := map[string][]string{}
1024+
1025+
// single entry in data
1026+
if data, ok := content["data"].(map[string]interface{}); ok {
1027+
errors := replaceAttributes(&queryParams, &data)
1028+
for t, v := range errors {
1029+
wrongFields[t] = v
1030+
}
1031+
}
1032+
1033+
// data can be a slice too
1034+
if datas, ok := content["data"].([]map[string]interface{}); ok {
1035+
for index, data := range datas {
1036+
errors := replaceAttributes(&queryParams, &data)
1037+
for t, v := range errors {
1038+
wrongFields[t] = v
1039+
}
1040+
datas[index] = data
1041+
}
1042+
}
1043+
1044+
// included slice
1045+
if included, ok := content["included"].([]map[string]interface{}); ok {
1046+
for index, include := range included {
1047+
errors := replaceAttributes(&queryParams, &include)
1048+
for t, v := range errors {
1049+
wrongFields[t] = v
1050+
}
1051+
included[index] = include
1052+
}
1053+
}
1054+
1055+
if len(wrongFields) > 0 {
1056+
httpError := NewHTTPError(nil, "Some requested fields were invalid", http.StatusBadRequest)
1057+
for k, v := range wrongFields {
1058+
for _, field := range v {
1059+
httpError.Errors = append(httpError.Errors, Error{
1060+
Status: "Bad Request",
1061+
Code: codeInvalidQueryFields,
1062+
Title: fmt.Sprintf(`Field "%s" does not exist for type "%s"`, field, k),
1063+
Detail: "Please make sure you do only request existing fields",
1064+
Source: &ErrorSource{
1065+
Parameter: fmt.Sprintf("fields[%s]", k),
1066+
},
1067+
})
1068+
}
1069+
}
1070+
return nil, httpError
1071+
}
1072+
}
1073+
return resp, nil
1074+
}
1075+
1076+
func parseQueryFields(query *url.Values) (result map[string][]string) {
1077+
result = map[string][]string{}
1078+
for name, param := range *query {
1079+
matches := queryFieldsRegex.FindStringSubmatch(name)
1080+
if len(matches) > 1 {
1081+
match := matches[1]
1082+
result[match] = strings.Split(param[0], ",")
1083+
}
1084+
}
1085+
1086+
return
1087+
}
1088+
1089+
func filterAttributes(attributes map[string]interface{}, fields []string) (filteredAttributes map[string]interface{}, wrongFields []string) {
1090+
wrongFields = []string{}
1091+
filteredAttributes = map[string]interface{}{}
1092+
1093+
for _, field := range fields {
1094+
if attribute, ok := attributes[field]; ok {
1095+
filteredAttributes[field] = attribute
1096+
} else {
1097+
wrongFields = append(wrongFields, field)
1098+
}
1099+
}
1100+
1101+
return
1102+
}
1103+
1104+
func replaceAttributes(query *map[string][]string, entry *map[string]interface{}) map[string][]string {
1105+
fieldType := (*entry)["type"].(string)
1106+
fields := (*query)[fieldType]
1107+
if len(fields) > 0 {
1108+
if attributes, ok := (*entry)["attributes"]; ok {
1109+
var wrongFields []string
1110+
(*entry)["attributes"], wrongFields = filterAttributes(attributes.(map[string]interface{}), fields)
1111+
if len(wrongFields) > 0 {
1112+
return map[string][]string{
1113+
fieldType: wrongFields,
1114+
}
1115+
}
1116+
}
1117+
}
1118+
1119+
return nil
1120+
}
1121+
10051122
func selectContentMarshaler(r *http.Request, marshalers map[string]ContentMarshaler) (marshaler ContentMarshaler, contentType string) {
10061123
if _, found := r.Header["Accept"]; found {
10071124
var contentTypes []string

api_test.go

Lines changed: 205 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ func (b Banana) GetID() string {
199199
type User struct {
200200
ID string `jsonapi:"-"`
201201
Name string
202+
Info string
202203
}
203204

204205
func (u User) GetID() string {
@@ -524,6 +525,7 @@ var _ = Describe("RestHandler", func() {
524525
"type": "users",
525526
"attributes": map[string]interface{}{
526527
"name": "Dieter",
528+
"info": "",
527529
},
528530
},
529531
{
@@ -650,7 +652,8 @@ var _ = Describe("RestHandler", func() {
650652
"id": "1",
651653
"type": "users",
652654
"attributes": {
653-
"name": "Dieter"
655+
"name": "Dieter",
656+
"info": ""
654657
}
655658
}}`))
656659
})
@@ -1587,4 +1590,205 @@ var _ = Describe("RestHandler", func() {
15871590
Expect(rec.Body.Bytes()).To(ContainSubstring(expected))
15881591
})
15891592
})
1593+
1594+
Context("Sparse Fieldsets", func() {
1595+
var (
1596+
source *fixtureSource
1597+
api *API
1598+
rec *httptest.ResponseRecorder
1599+
)
1600+
1601+
BeforeEach(func() {
1602+
author := User{ID: "666", Name: "Tester", Info: "Is curious about testing"}
1603+
source = &fixtureSource{map[string]*Post{
1604+
"1": {ID: "1", Title: "Nice Post", Value: null.FloatFrom(13.37), Author: &author},
1605+
}, false}
1606+
api = NewAPI("")
1607+
api.AddResource(Post{}, source)
1608+
rec = httptest.NewRecorder()
1609+
})
1610+
1611+
It("only returns requested post fields for single post", func() {
1612+
req, err := http.NewRequest("GET", "/posts/1?fields[posts]=title,value", nil)
1613+
Expect(err).ToNot(HaveOccurred())
1614+
api.Handler().ServeHTTP(rec, req)
1615+
Expect(rec.Code).To(Equal(http.StatusOK))
1616+
Expect(rec.Body.Bytes()).To(MatchJSON(`
1617+
{"data": {
1618+
"id": "1",
1619+
"type": "posts",
1620+
"attributes": {
1621+
"title": "Nice Post",
1622+
"value": 13.37
1623+
},
1624+
"relationships": {
1625+
"author": {
1626+
"data": {
1627+
"id": "666",
1628+
"type": "users"
1629+
},
1630+
"links": {
1631+
"related": "/posts/1/author",
1632+
"self": "/posts/1/relationships/author"
1633+
}
1634+
},
1635+
"bananas": {
1636+
"data": [],
1637+
"links": {
1638+
"related": "/posts/1/bananas",
1639+
"self": "/posts/1/relationships/bananas"
1640+
}
1641+
},
1642+
"comments": {
1643+
"data": [],
1644+
"links": {
1645+
"related": "/posts/1/comments",
1646+
"self": "/posts/1/relationships/comments"
1647+
}
1648+
}
1649+
}
1650+
},
1651+
"included": [
1652+
{
1653+
"attributes": {
1654+
"info": "Is curious about testing",
1655+
"name": "Tester"
1656+
},
1657+
"id": "666",
1658+
"type": "users"
1659+
}
1660+
]
1661+
}`))
1662+
})
1663+
1664+
It("FindOne: only returns requested post field for single post and includes", func() {
1665+
req, err := http.NewRequest("GET", "/posts/1?fields[posts]=title&fields[users]=name", nil)
1666+
Expect(err).ToNot(HaveOccurred())
1667+
api.Handler().ServeHTTP(rec, req)
1668+
Expect(rec.Code).To(Equal(http.StatusOK))
1669+
Expect(rec.Body.Bytes()).To(MatchJSON(`
1670+
{"data": {
1671+
"id": "1",
1672+
"type": "posts",
1673+
"attributes": {
1674+
"title": "Nice Post"
1675+
},
1676+
"relationships": {
1677+
"author": {
1678+
"data": {
1679+
"id": "666",
1680+
"type": "users"
1681+
},
1682+
"links": {
1683+
"related": "/posts/1/author",
1684+
"self": "/posts/1/relationships/author"
1685+
}
1686+
},
1687+
"bananas": {
1688+
"data": [],
1689+
"links": {
1690+
"related": "/posts/1/bananas",
1691+
"self": "/posts/1/relationships/bananas"
1692+
}
1693+
},
1694+
"comments": {
1695+
"data": [],
1696+
"links": {
1697+
"related": "/posts/1/comments",
1698+
"self": "/posts/1/relationships/comments"
1699+
}
1700+
}
1701+
}
1702+
},
1703+
"included": [
1704+
{
1705+
"attributes": {
1706+
"name": "Tester"
1707+
},
1708+
"id": "666",
1709+
"type": "users"
1710+
}
1711+
]
1712+
}`))
1713+
})
1714+
1715+
It("FindAll: only returns requested post field for single post and includes", func() {
1716+
req, err := http.NewRequest("GET", "/posts?fields[posts]=title&fields[users]=name", nil)
1717+
Expect(err).ToNot(HaveOccurred())
1718+
api.Handler().ServeHTTP(rec, req)
1719+
Expect(rec.Code).To(Equal(http.StatusOK))
1720+
Expect(rec.Body.Bytes()).To(MatchJSON(`
1721+
{"data": [{
1722+
"id": "1",
1723+
"type": "posts",
1724+
"attributes": {
1725+
"title": "Nice Post"
1726+
},
1727+
"relationships": {
1728+
"author": {
1729+
"data": {
1730+
"id": "666",
1731+
"type": "users"
1732+
},
1733+
"links": {
1734+
"related": "/posts/1/author",
1735+
"self": "/posts/1/relationships/author"
1736+
}
1737+
},
1738+
"bananas": {
1739+
"data": [],
1740+
"links": {
1741+
"related": "/posts/1/bananas",
1742+
"self": "/posts/1/relationships/bananas"
1743+
}
1744+
},
1745+
"comments": {
1746+
"data": [],
1747+
"links": {
1748+
"related": "/posts/1/comments",
1749+
"self": "/posts/1/relationships/comments"
1750+
}
1751+
}
1752+
}
1753+
}],
1754+
"included": [
1755+
{
1756+
"attributes": {
1757+
"name": "Tester"
1758+
},
1759+
"id": "666",
1760+
"type": "users"
1761+
}
1762+
]
1763+
}`))
1764+
})
1765+
1766+
It("Summarize all invalid field query parameters as error", func() {
1767+
req, err := http.NewRequest("GET", "/posts?fields[posts]=title,nonexistent&fields[users]=name,title,fluffy,pink", nil)
1768+
Expect(err).ToNot(HaveOccurred())
1769+
api.Handler().ServeHTTP(rec, req)
1770+
Expect(rec.Code).To(Equal(http.StatusBadRequest))
1771+
error := HTTPError{}
1772+
err = json.Unmarshal(rec.Body.Bytes(), &error)
1773+
Expect(err).ToNot(HaveOccurred())
1774+
1775+
expectedError := func(field, objType string) Error {
1776+
return Error{
1777+
Status: "Bad Request",
1778+
Code: codeInvalidQueryFields,
1779+
Title: fmt.Sprintf(`Field "%s" does not exist for type "%s"`, field, objType),
1780+
Detail: "Please make sure you do only request existing fields",
1781+
Source: &ErrorSource{
1782+
Parameter: fmt.Sprintf("fields[%s]", objType),
1783+
},
1784+
}
1785+
}
1786+
1787+
Expect(error.Errors).To(HaveLen(4))
1788+
Expect(error.Errors).To(ContainElement(expectedError("nonexistent", "posts")))
1789+
Expect(error.Errors).To(ContainElement(expectedError("title", "users")))
1790+
Expect(error.Errors).To(ContainElement(expectedError("fluffy", "users")))
1791+
Expect(error.Errors).To(ContainElement(expectedError("pink", "users")))
1792+
})
1793+
})
15901794
})

0 commit comments

Comments
 (0)