Skip to content

Commit 39eea9c

Browse files
committed
sparse fieldsets filtering support
if the appropriate query parameters are available. The api goes through all the attributes and only includes the ones from the query parameter. For example: `?fields[posts]=title,content` If there are invalid fields in the query, errors with all of them will be returned
1 parent 4b5c8ff commit 39eea9c

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)