├── .gitignore ├── LICENSE ├── README.md ├── example.go ├── example_mongo.go ├── expander ├── expander.go └── expander_test.go └── wercker.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | 4 | src 5 | pkg 6 | bin 7 | 8 | *.out 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright [2014] [Isa Goksu] 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go REST Expander 2 | 3 | A tiny library to allow your RESTful resources to be expanded and/or filtered. If you don't know what resource expansions are, I'd recommend reading [Linking and Resource Expansion](https://stormpath.com/blog/linking-and-resource-expansion-rest-api-tips/) from *StormPath* blog. Although I have a different implementation, it's still a good read... 4 | 5 | [![wercker status](https://app.wercker.com/status/9daffd357de5205f4f9e5f185dda36a8/s "wercker status")](https://app.wercker.com/project/bykey/9daffd357de5205f4f9e5f185dda36a8) 6 | 7 | My implementation is basically as following. Assuming you have below contact detail: 8 | 9 | ``` 10 | GET http://localhost:9003/contacts/id/3 11 | ``` 12 | 13 | call returns 14 | 15 | ```json 16 | { 17 | "addresses": [ 18 | { 19 | "ref": "http://localhost:9002/addresses/id/147", 20 | "rel": "home", 21 | "verb": "GET" 22 | }, 23 | { 24 | "ref": "http://localhost:9002/addresses/id/412", 25 | "rel": "business", 26 | "verb": "GET" 27 | } 28 | ], 29 | "cell": "+1 (312) 888-44444", 30 | "group": { 31 | "ref": "http://localhost:9001/groups/id/7", 32 | "rel": "family", 33 | "verb": "GET" 34 | }, 35 | "id": 3, 36 | "name": "John Doe" 37 | } 38 | ``` 39 | 40 | You can filter out the fields by calling: 41 | 42 | ``` 43 | GET http://localhost:9003/contacts/id/3?filter=name,cell 44 | ``` 45 | 46 | will return 47 | 48 | ```json 49 | { 50 | "cell": "+1 (312) 888-44444", 51 | "name": "John Doe" 52 | } 53 | ``` 54 | 55 | If you'd like to expand the addresses, just call: 56 | 57 | ``` 58 | GET http://localhost:9003/contacts/id/3?expand=addresses 59 | ``` 60 | 61 | will return 62 | 63 | ```json 64 | { 65 | "addresses": [ 66 | { 67 | "city": { 68 | "ref": "http://localhost:9003/cities/id/1", 69 | "rel": "home", 70 | "verb": "GET" 71 | }, 72 | "id": 147 73 | }, 74 | { 75 | "city": { 76 | "ref": "http://localhost:9003/cities/id/2", 77 | "rel": "business", 78 | "verb": "GET" 79 | }, 80 | "id": 412 81 | } 82 | ], 83 | "cell": "+1 (312) 888-44444", 84 | "group": { 85 | "ref": "http://localhost:9001/groups/id/7", 86 | "rel": "family", 87 | "verb": "GET" 88 | }, 89 | "id": 3, 90 | "name": "John Doe" 91 | } 92 | ``` 93 | 94 | If you want to filter out the expanded result as well, you could do this easily by: 95 | 96 | ``` 97 | GET http://localhost:9003/contacts/id/3?expand=addresses&filter=name,addresses(id,city) 98 | ``` 99 | 100 | will give you 101 | 102 | ```json 103 | { 104 | "addresses": [ 105 | { 106 | "city": { 107 | "ref": "http://localhost:9003/cities/id/1", 108 | "rel": "home", 109 | "verb": "GET" 110 | }, 111 | "id": 147 112 | }, 113 | { 114 | "city": { 115 | "ref": "http://localhost:9003/cities/id/2", 116 | "rel": "business", 117 | "verb": "GET" 118 | }, 119 | "id": 412 120 | } 121 | ], 122 | "name": "John Doe" 123 | } 124 | ``` 125 | 126 | If you wanna go nuts, you can always try something like: 127 | 128 | ``` 129 | GET http://localhost:9003/contacts/id/3?expand=*&filter=name,addresses(city(name)) 130 | ``` 131 | 132 | You'll get: 133 | 134 | ```json 135 | { 136 | "addresses": [ 137 | { 138 | "city": { 139 | "name": "Gotham City" 140 | } 141 | }, 142 | { 143 | "city": { 144 | "name": "Atlantis" 145 | } 146 | } 147 | ], 148 | "name": "John Doe" 149 | } 150 | ``` 151 | 152 | Filter default is showing all results, and expansion default is expanding nothing. If you wanna expand everything try `*` for it. 153 | 154 | As you can see, it's just my weekend project. So feel free to give feedback or open issues. I'll try my best to fix them in ASAP. 155 | 156 | ## Mongo DBRef Expansions 157 | 158 | I also added a functionality for expanding mongo DBRef fields as well. So if you are using `mgo`, you can easily expand and resolve the Mongo references as well. To do so, you need to set the configuration like: 159 | 160 | ```go 161 | expander.ExpanderConfig = expander.Configuration{ 162 | UsingMongo: true, 163 | IdURIs: map[string]string { 164 | "people": "http://localhost:9000/contacts/id", 165 | }, 166 | } 167 | ``` 168 | 169 | Here `IdURIs` is basically a map of collection -> base URI of your resources. I added another example named `example_mongo` in the project directory. You can check it as well for understanding how it works. It's pretty simple. 170 | 171 | By default, it won't expand mongo references. 172 | 173 | ## Installation 174 | 175 | ```bash 176 | go get github.com/isa/go-rest-expander/expander 177 | ``` 178 | 179 | ## Usage 180 | 181 | Basically all you need to do is calling the Expand function with the right filters and expansion parameter. 182 | 183 | ```go 184 | expanded := expander.Expand(myData, expansion, filter) 185 | ``` 186 | 187 | That's it. You can always check the `example.go` file in the root directory for a running example. Just run the example by: 188 | 189 | ```bash 190 | go run example.go 191 | ``` 192 | 193 | This will create 4 dummy endpoints (contacts, addresses, cities, groups). Basically the above examples. You can use your favorite browser or curl to try above examples on the same endpoints. 194 | 195 | Or if you are lazy like me, just copy paste following and then you are good to go. 196 | 197 | ```go 198 | package main 199 | 200 | import ( 201 | "github.com/isa/go-rest-expander/expander" 202 | "fmt" 203 | "encoding/json" 204 | "net/http" 205 | ) 206 | 207 | type Contact struct { 208 | Id int `json:"id"` 209 | Name string `json:"name"` 210 | Cell string `json:"cell"` 211 | Group Link `json:"group"` 212 | Addresses []Link `json:"addresses"` 213 | } 214 | 215 | type Address struct { 216 | Id int `json:"id"` 217 | City Link `json:"city"` 218 | } 219 | 220 | type City struct { 221 | Id int `json:"id"` 222 | Name string `json:"name"` 223 | } 224 | 225 | type Group struct { 226 | Id int `json:"id"` 227 | Name string `json:"name"` 228 | Description string `json:"description"` 229 | } 230 | 231 | type Link struct { 232 | Ref string `json:"ref"` 233 | Rel string `json:"rel"` 234 | Verb string `json:"verb"` 235 | } 236 | 237 | func contactHandler(w http.ResponseWriter, r *http.Request) { 238 | g := Link{"http://localhost:9001/groups/id/7", "family", "GET"} 239 | a1 := Link{"http://localhost:9002/addresses/id/147", "home", "GET"} 240 | a2 := Link{"http://localhost:9002/addresses/id/412", "business", "GET"} 241 | 242 | c := Contact{3, "John Doe", "+1 (312) 888-44444", g, []Link{a1, a2}} 243 | 244 | expansion, filter := r.FormValue("expand"), r.FormValue("filter") 245 | expanded := expander.Expand(c, expansion, filter) 246 | result, _ := json.Marshal(expanded) 247 | 248 | fmt.Fprintf(w, string(result)) 249 | } 250 | 251 | func groupHandler(w http.ResponseWriter, r *http.Request) { 252 | g := Group{7, "Family", "My family members"} 253 | result, _ := json.Marshal(g) 254 | 255 | fmt.Fprintf(w, string(result)) 256 | } 257 | 258 | func addressHandler(w http.ResponseWriter, r *http.Request) { 259 | addresses := map[string]Address { 260 | "147": Address{147, Link{"http://localhost:9003/cities/id/1", "home", "GET"}}, 261 | "412": Address{412, Link{"http://localhost:9003/cities/id/2", "business", "GET"}}, 262 | } 263 | result, _ := json.Marshal(addresses[r.URL.Path[14:]]) 264 | 265 | fmt.Fprintf(w, string(result)) 266 | } 267 | 268 | func cityHandler(w http.ResponseWriter, r *http.Request) { 269 | cities := map[string]City { 270 | "1": City{1, "Gotham City"}, 271 | "2": City{2, "Atlantis"}, 272 | } 273 | result, _ := json.Marshal(cities[r.URL.Path[11:]]) 274 | 275 | fmt.Fprintf(w, string(result)) 276 | } 277 | 278 | func main() { 279 | go func() { 280 | http.HandleFunc("/contacts/id/3", contactHandler) 281 | http.ListenAndServe(":9000", nil) 282 | }() 283 | 284 | go func() { 285 | http.HandleFunc("/groups/id/7", groupHandler) 286 | http.ListenAndServe(":9001", nil) 287 | }() 288 | 289 | go func() { 290 | http.HandleFunc("/addresses/id/147", addressHandler) 291 | http.HandleFunc("/addresses/id/412", addressHandler) 292 | http.ListenAndServe(":9002", nil) 293 | }() 294 | 295 | go func() { 296 | http.HandleFunc("/cities/id/1", cityHandler) 297 | http.HandleFunc("/cities/id/2", cityHandler) 298 | http.ListenAndServe(":9003", nil) 299 | }() 300 | 301 | select {} 302 | } 303 | 304 | ``` 305 | ## Caching 306 | 307 | Expander includes a caching mechanism to cache HTTP-calls. It is **deactivated by default**, to activate it just add the following to your configuration: 308 | 309 | ```go 310 | expander.ExpanderConfig = expander.Configuration{ 311 | UsingCache: true, 312 | CacheExpInSeconds: 86400, // 24h 313 | ConnectionTimeoutInS = 2, 314 | ... 315 | } 316 | ``` 317 | CacheExpInSeconds is the maximum time that an entry is cached. ConnectionTimeoutInS is the time until a http-request is canceled 318 | 319 | ## Developers 320 | 321 | I use [GoConvey](http://goconvey.co/) for testing. 322 | 323 | ## License 324 | 325 | Licensed under [Apache 2.0](LICENSE). 326 | -------------------------------------------------------------------------------- /example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/isa/go-rest-expander/expander" 5 | "fmt" 6 | "encoding/json" 7 | "net/http" 8 | ) 9 | 10 | type Contact struct { 11 | Id int `json:"id"` 12 | Name string `json:"name"` 13 | Cell string `json:"cell"` 14 | Group Link `json:"group"` 15 | Addresses []Link `json:"addresses"` 16 | } 17 | 18 | type Address struct { 19 | Id int `json:"id"` 20 | City Link `json:"city"` 21 | } 22 | 23 | type City struct { 24 | Id int `json:"id"` 25 | Name string `json:"name"` 26 | } 27 | 28 | type Group struct { 29 | Id int `json:"id"` 30 | Name string `json:"name"` 31 | Description string `json:"description"` 32 | } 33 | 34 | type Link struct { 35 | Ref string `json:"ref"` 36 | Rel string `json:"rel"` 37 | Verb string `json:"verb"` 38 | } 39 | 40 | func contactHandler(w http.ResponseWriter, r *http.Request) { 41 | g := Link{"http://localhost:9001/groups/id/7", "family", "GET"} 42 | a1 := Link{"http://localhost:9002/addresses/id/147", "home", "GET"} 43 | a2 := Link{"http://localhost:9002/addresses/id/412", "business", "GET"} 44 | 45 | c := Contact{3, "John Doe", "+1 (312) 888-44444", g, []Link{a1, a2}} 46 | 47 | expansion, filter := r.FormValue("expand"), r.FormValue("filter") 48 | expanded := expander.Expand(c, expansion, filter) 49 | result, _ := json.Marshal(expanded) 50 | 51 | fmt.Fprintf(w, string(result)) 52 | } 53 | 54 | func groupHandler(w http.ResponseWriter, r *http.Request) { 55 | g := Group{7, "Family", "My family members"} 56 | result, _ := json.Marshal(g) 57 | 58 | fmt.Fprintf(w, string(result)) 59 | } 60 | 61 | func addressHandler(w http.ResponseWriter, r *http.Request) { 62 | addresses := map[string]Address { 63 | "147": Address{147, Link{"http://localhost:9003/cities/id/1", "home", "GET"}}, 64 | "412": Address{412, Link{"http://localhost:9003/cities/id/2", "business", "GET"}}, 65 | } 66 | result, _ := json.Marshal(addresses[r.URL.Path[14:]]) 67 | 68 | fmt.Fprintf(w, string(result)) 69 | } 70 | 71 | func cityHandler(w http.ResponseWriter, r *http.Request) { 72 | cities := map[string]City { 73 | "1": City{1, "Gotham City"}, 74 | "2": City{2, "Atlantis"}, 75 | } 76 | result, _ := json.Marshal(cities[r.URL.Path[11:]]) 77 | 78 | fmt.Fprintf(w, string(result)) 79 | } 80 | 81 | func main() { 82 | go func() { 83 | http.HandleFunc("/contacts/id/3", contactHandler) 84 | http.ListenAndServe(":9000", nil) 85 | }() 86 | 87 | go func() { 88 | http.HandleFunc("/groups/id/7", groupHandler) 89 | http.ListenAndServe(":9001", nil) 90 | }() 91 | 92 | go func() { 93 | http.HandleFunc("/addresses/id/147", addressHandler) 94 | http.HandleFunc("/addresses/id/412", addressHandler) 95 | http.ListenAndServe(":9002", nil) 96 | }() 97 | 98 | go func() { 99 | http.HandleFunc("/cities/id/1", cityHandler) 100 | http.HandleFunc("/cities/id/2", cityHandler) 101 | http.ListenAndServe(":9003", nil) 102 | }() 103 | 104 | select {} 105 | } 106 | -------------------------------------------------------------------------------- /example_mongo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/isa/go-rest-expander/expander" 7 | "labix.org/v2/mgo" 8 | "labix.org/v2/mgo/bson" 9 | "net/http" 10 | "regexp" 11 | ) 12 | 13 | type Group struct { 14 | Name string 15 | Folks []mgo.DBRef 16 | } 17 | 18 | type Person struct { 19 | Id interface{} "_id" 20 | Name string 21 | Phone string 22 | } 23 | 24 | func groupHandler(w http.ResponseWriter, r *http.Request) { 25 | session, _ := mgo.Dial("localhost") 26 | defer session.Close() 27 | 28 | session.SetMode(mgo.Monotonic, true) 29 | g := session.DB("test").C("groups") 30 | 31 | group := Group{} 32 | _ = g.Find(bson.M{"name": "Family"}).One(&group) 33 | 34 | expander.ExpanderConfig = expander.Configuration{ 35 | UsingMongo: true, 36 | IdURIs: map[string]string{ 37 | "people": "http://localhost:9000/contacts/id", 38 | }, 39 | } 40 | expanded := expander.Expand(group, r.FormValue("expand"), r.FormValue("filter")) 41 | result, _ := json.Marshal(expanded) 42 | fmt.Fprintf(w, string(result)) 43 | } 44 | 45 | func contactHandler(w http.ResponseWriter, r *http.Request) { 46 | session, _ := mgo.Dial("localhost") 47 | defer session.Close() 48 | 49 | session.SetMode(mgo.Monotonic, true) 50 | c := session.DB("test").C("people") 51 | 52 | contact := Person{} 53 | _ = c.FindId(bson.ObjectIdHex(r.URL.Path[13:])).One(&contact) 54 | 55 | result, _ := json.Marshal(contact) 56 | fmt.Fprintf(w, string(result)) 57 | } 58 | 59 | type route struct { 60 | pattern *regexp.Regexp 61 | handler http.Handler 62 | } 63 | 64 | type RegexpHandler struct { 65 | routes []*route 66 | } 67 | 68 | func (h *RegexpHandler) AddRoute(pattern string, handler func(http.ResponseWriter, *http.Request)) { 69 | h.routes = append(h.routes, &route{regexp.MustCompile(pattern), http.HandlerFunc(handler)}) 70 | } 71 | 72 | func (h *RegexpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 73 | for _, route := range h.routes { 74 | if route.pattern.MatchString(r.URL.Path) { 75 | route.handler.ServeHTTP(w, r) 76 | return 77 | } 78 | } 79 | 80 | http.NotFound(w, r) 81 | } 82 | 83 | func main() { 84 | session, _ := mgo.Dial("localhost") 85 | defer session.Close() 86 | 87 | session.SetMode(mgo.Monotonic, true) 88 | c := session.DB("test").C("people") 89 | g := session.DB("test").C("groups") 90 | 91 | id1, id2 := bson.NewObjectId(), bson.NewObjectId() 92 | refs := []mgo.DBRef{mgo.DBRef{"people", id1, "test"}, mgo.DBRef{"people", id2, "test"}} 93 | _ = c.Insert(&Person{id1, "Ale", "+55 53 8116 9639"}, &Person{id2, "Cla", "+55 53 8402 8510"}) 94 | _ = g.Insert(&Group{"Family", refs}) 95 | 96 | go func() { 97 | regexMux := new(RegexpHandler) 98 | regexMux.AddRoute("/contacts/id/.*", contactHandler) 99 | regexMux.AddRoute("/groups/id/.*", groupHandler) 100 | http.ListenAndServe(":9000", regexMux) 101 | }() 102 | 103 | select {} 104 | } 105 | -------------------------------------------------------------------------------- /expander/expander.go: -------------------------------------------------------------------------------- 1 | package expander 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "reflect" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "time" 14 | "net" 15 | "errors" 16 | 17 | "github.com/golang/groupcache/lru" 18 | ) 19 | 20 | const ( 21 | REF_KEY = "ref" 22 | REL_KEY = "rel" 23 | VERB_KEY = "verb" 24 | COLLECTION_KEY = "Collection" 25 | ) 26 | 27 | type Configuration struct { 28 | UsingCache bool 29 | UsingMongo bool 30 | IdURIs map[string]string 31 | CacheExpInSeconds int64 32 | ConnectionTimeoutInS int 33 | } 34 | 35 | var ExpanderConfig Configuration = Configuration{ 36 | UsingMongo: false, 37 | UsingCache: false, 38 | CacheExpInSeconds: 86400, // = 24 hours 39 | ConnectionTimeoutInS: 2, 40 | } 41 | 42 | var Cache *lru.Cache = lru.New(250) 43 | var CacheMutex = sync.Mutex{} 44 | var client http.Client 45 | var timeout = time.Duration(2 * time.Second) 46 | var httpClientIsInitialized = false 47 | var initializingHttpClient = false 48 | var initializerMutex = sync.Mutex{} 49 | 50 | func dialTimeout(network, addr string) (net.Conn, error) { 51 | return net.DialTimeout(network, addr, timeout) 52 | } 53 | 54 | func Init() { 55 | client = http.Client{} 56 | 57 | client.Timeout = time.Duration(ExpanderConfig.ConnectionTimeoutInS)*time.Second 58 | 59 | httpClientIsInitialized = true 60 | } 61 | 62 | type CacheEntry struct { 63 | Timestamp int64 64 | Data string 65 | } 66 | 67 | type DBRef struct { 68 | Collection string 69 | Id interface{} 70 | Database string 71 | } 72 | 73 | type ObjectId interface { 74 | Hex() string 75 | } 76 | 77 | type Filter struct { 78 | Children Filters 79 | Value string 80 | } 81 | 82 | type Filters []Filter 83 | 84 | func (m Filters) Contains(v string) bool { 85 | for _, m := range m { 86 | if v == m.Value { 87 | return true 88 | } 89 | } 90 | 91 | return false 92 | } 93 | 94 | func (m Filters) IsEmpty() bool { 95 | return len(m) == 0 96 | } 97 | 98 | func (m Filters) Get(v string) Filter { 99 | var result Filter 100 | 101 | if m.IsEmpty() { 102 | return result 103 | } 104 | 105 | for _, m := range m { 106 | if v == m.Value { 107 | return m 108 | } 109 | } 110 | 111 | return result 112 | } 113 | 114 | func resolveFilters(expansion, fields string) (expansionFilter Filters, fieldFilter Filters, recursiveExpansion bool, err error) { 115 | if !validateFilterFormat(expansion) { 116 | err = errors.New("expansionFilter for filtering was not correct") 117 | return 118 | } 119 | if !validateFilterFormat(fields) { 120 | err = errors.New("fieldFilter for filtering was not correct") 121 | return 122 | } 123 | 124 | fieldFilter, _ = buildFilterTree(fields) 125 | 126 | if expansion != "*" { 127 | expansionFilter, _ = buildFilterTree(expansion) 128 | } else if fields != "*" && fields != "" { 129 | expansionFilter, _ = buildFilterTree(fields) 130 | } else { 131 | recursiveExpansion = true 132 | } 133 | return 134 | } 135 | 136 | //TODO: TagFields & BSONFields 137 | func Expand(data interface{}, expansion, fields string) map[string]interface{} { 138 | if ExpanderConfig.UsingMongo && len(ExpanderConfig.IdURIs) == 0 { 139 | fmt.Println("Warning: Cannot use mongo flag without proper IdURIs given!") 140 | } 141 | if ExpanderConfig.UsingCache && ExpanderConfig.CacheExpInSeconds == 0 { 142 | fmt.Println("Warning: Cannot use Cache with expiration 0, cache will be useless!") 143 | } 144 | 145 | expansionFilter, fieldFilter, recursiveExpansion, err := resolveFilters(expansion, fields) 146 | if err != nil { 147 | expansionFilter = Filters{} 148 | fieldFilter = Filters{} 149 | fmt.Printf("Warning: Filter was not correct, expansionFilter: '%v' fieldFilter: '%v', error: %v \n", expansion, fields, err) 150 | } 151 | 152 | expanded := *walkByExpansion(data, expansionFilter, recursiveExpansion) 153 | 154 | filtered := walkByFilter(expanded, fieldFilter) 155 | 156 | return filtered 157 | } 158 | 159 | func ExpandArray(data interface{}, expansion, fields string) []interface{} { 160 | if ExpanderConfig.UsingMongo && len(ExpanderConfig.IdURIs) == 0 { 161 | fmt.Println("Warning: Cannot use mongo flag without proper IdURIs given!") 162 | } 163 | if ExpanderConfig.UsingCache && ExpanderConfig.CacheExpInSeconds == 0 { 164 | fmt.Println("Warning: Cannot use Cache with expiration 0, cache will be useless!") 165 | } 166 | 167 | expansionFilter, fieldFilter, recursiveExpansion, err := resolveFilters(expansion, fields) 168 | if err != nil { 169 | expansionFilter = Filters{} 170 | fieldFilter = Filters{} 171 | fmt.Printf("Warning: Filter was not correct, expansionFilter: '%v' fieldFilter: '%v', error: %v \n", expansionFilter, fieldFilter, err) 172 | } 173 | 174 | var result []interface{} 175 | 176 | if data == nil { 177 | return result 178 | } 179 | 180 | v := reflect.ValueOf(data) 181 | switch data.(type) { 182 | case reflect.Value: 183 | v = data.(reflect.Value) 184 | } 185 | 186 | if v.Kind() != reflect.Slice { 187 | return result 188 | } 189 | 190 | v = v.Slice(0, v.Len()) 191 | for i := 0; i < v.Len(); i++ { 192 | arrayItem := *walkByExpansion(v.Index(i), expansionFilter, recursiveExpansion) 193 | arrayItem = walkByFilter(arrayItem, fieldFilter) 194 | result = append(result, arrayItem) 195 | } 196 | return result 197 | } 198 | 199 | func walkByFilter(data map[string]interface{}, filters Filters) map[string]interface{} { 200 | result := make(map[string]interface{}) 201 | 202 | if data == nil { 203 | return result 204 | } 205 | 206 | for k, v := range data { 207 | if filters.IsEmpty() || filters.Contains(k) { 208 | ft := reflect.ValueOf(v) 209 | 210 | result[k] = v 211 | subFilters := filters.Get(k).Children 212 | 213 | if v == nil { 214 | continue 215 | } 216 | 217 | switch ft.Type().Kind() { 218 | case reflect.Map: 219 | result[k] = walkByFilter(v.(map[string]interface{}), subFilters) 220 | case reflect.Slice: 221 | if ft.Len() == 0 { 222 | continue 223 | } 224 | 225 | switch ft.Index(0).Kind() { 226 | case reflect.Map: 227 | children := make([]map[string]interface{}, 0) 228 | for _, child := range v.([]map[string]interface{}) { 229 | item := walkByFilter(child, subFilters) 230 | children = append(children, item) 231 | } 232 | result[k] = children 233 | default: 234 | children := make([]interface{}, 0) 235 | for _, child := range v.([]interface{}) { 236 | cft := reflect.TypeOf(child) 237 | 238 | if cft.Kind() == reflect.Map { 239 | item := walkByFilter(child.(map[string]interface{}), subFilters) 240 | children = append(children, item) 241 | } else { 242 | children = append(children, child) 243 | } 244 | } 245 | result[k] = children 246 | } 247 | } 248 | } 249 | } 250 | 251 | return result 252 | } 253 | 254 | func walkByExpansion(data interface{}, filters Filters, recursive bool) *map[string]interface{} { 255 | result := make(map[string]interface{}) 256 | 257 | if data == nil { 258 | return &result 259 | } 260 | 261 | v := reflect.ValueOf(data) 262 | switch data.(type) { 263 | case reflect.Value: 264 | v = data.(reflect.Value) 265 | } 266 | if v.Type().Kind() == reflect.Ptr { 267 | v = v.Elem() 268 | } 269 | 270 | // var resultWriteMutex = sync.Mutex{} 271 | var writeToResult = func(key string, value interface{}) { 272 | //resultWriteMutex.Lock() 273 | result[key] = value 274 | //resultWriteMutex.Unlock() 275 | } 276 | 277 | // check if root is db ref 278 | if isMongoDBRef(v) && recursive { 279 | uri := buildReferenceURI(v) 280 | key := v.Type().Field(1).Name 281 | placeholder := make(map[string]interface{}) 282 | resource, _ := getResourceFrom(uri, filters.Get(key).Children, recursive) 283 | for k, v := range resource { 284 | placeholder[k] = v 285 | } 286 | return &placeholder 287 | } 288 | 289 | for i := 0; i < v.NumField(); i++ { 290 | f := v.Field(i) 291 | ft := v.Type().Field(i) 292 | 293 | if f.Kind() == reflect.Ptr { 294 | f = f.Elem() 295 | } 296 | 297 | key := ft.Name 298 | tag := ft.Tag.Get("json") 299 | if tag != "" { 300 | key = strings.Split(tag, ",")[0] 301 | } 302 | 303 | options := func() (bool, string) { 304 | return recursive, key 305 | } 306 | 307 | if isMongoDBRef(f) { 308 | if filters.Contains(key) || recursive { 309 | uri := buildReferenceURI(f) 310 | resource, ok := getResourceFrom(uri, filters.Get(key).Children, recursive) 311 | if ok && len(resource) > 0 { 312 | writeToResult(key, resource) 313 | }else { 314 | writeToResult(key, f.Interface()) 315 | } 316 | } else { 317 | writeToResult(key, f.Interface()) 318 | } 319 | } else { 320 | val := getValue(f, filters, options) 321 | writeToResult(key, val) 322 | switch val.(type) { 323 | case string: 324 | unquoted, err := strconv.Unquote(val.(string)) 325 | if err == nil { 326 | writeToResult(key, unquoted) 327 | } 328 | } 329 | 330 | if isReference(f) { 331 | if filters.Contains(key) || recursive { 332 | uri := getReferenceURI(f) 333 | resource, ok := getResourceFrom(uri, filters.Get(key).Children, recursive) 334 | if ok { 335 | writeToResult(key, resource) 336 | } 337 | } 338 | } 339 | } 340 | 341 | } 342 | 343 | return &result 344 | } 345 | 346 | func getValue(t reflect.Value, filters Filters, options func() (bool, string)) interface{} { 347 | recursive, parentKey := options() 348 | 349 | switch t.Kind() { 350 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 351 | return t.Int() 352 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 353 | return t.Uint() 354 | case reflect.Float32, reflect.Float64: 355 | return t.Float() 356 | case reflect.Bool: 357 | return t.Bool() 358 | case reflect.String: 359 | return t.String() 360 | case reflect.Slice: 361 | var result = []interface{}{} 362 | 363 | for i := 0; i < t.Len(); i++ { 364 | current := t.Index(i) 365 | 366 | if filters.Contains(parentKey) || recursive { 367 | if isReference(current) { 368 | uri := getReferenceURI(current) 369 | 370 | //TODO: this fails in case the resource cannot be resolved, because current is DBRef not map[string]interface{} 371 | result = append(result, current.Interface()) 372 | resource, ok := getResourceFrom(uri, filters.Get(parentKey).Children, recursive) 373 | if ok { 374 | result[i] = resource 375 | } 376 | } else if isMongoDBRef(current) { 377 | uri := buildReferenceURI(current) 378 | 379 | //TODO: this fails in case the resource cannot be resolved, because current is DBRef not map[string]interface{} 380 | result = append(result, current.Interface()) 381 | resource, ok := getResourceFrom(uri, filters.Get(parentKey).Children, recursive) 382 | if ok { 383 | result[i] = resource 384 | } 385 | } else { 386 | result = append(result, getValue(current, filters.Get(parentKey).Children, options)) 387 | } 388 | } else { 389 | result = append(result, getValue(current, filters.Get(parentKey).Children, options)) 390 | } 391 | } 392 | 393 | return result 394 | case reflect.Map: 395 | result := make(map[string]interface{}) 396 | 397 | for _, v := range t.MapKeys() { 398 | key := v.Interface().(string) 399 | result[key] = getValue(t.MapIndex(v), filters.Get(key).Children, options) 400 | } 401 | 402 | return result 403 | case reflect.Struct: 404 | val, ok := t.Interface().(json.Marshaler) 405 | if ok { 406 | bytes, err := val.(json.Marshaler).MarshalJSON() 407 | if err != nil { 408 | fmt.Println(err) 409 | } 410 | 411 | return string(bytes) 412 | } 413 | 414 | return *walkByExpansion(t, filters, recursive) 415 | default: 416 | return t.Interface() 417 | } 418 | 419 | return "" 420 | } 421 | 422 | func getResourceFrom(u string, filters Filters, recursive bool) (map[string]interface{}, bool) { 423 | ok := false 424 | uri, err := url.ParseRequestURI(u) 425 | var m map[string]interface{} 426 | 427 | if err == nil { 428 | content := getContentFrom(uri) 429 | err := json.Unmarshal([]byte(content), &m) 430 | if err != nil { 431 | return m, false 432 | } 433 | ok = true 434 | if hasReference(m) { 435 | return *expandChildren(m, filters, recursive), ok 436 | } 437 | } 438 | 439 | return m, ok 440 | } 441 | 442 | func expandChildren(m map[string]interface{}, filters Filters, recursive bool) *map[string]interface{} { 443 | result := make(map[string]interface{}) 444 | 445 | for key, v := range m { 446 | ft := reflect.TypeOf(v) 447 | result[key] = v 448 | if v == nil { 449 | continue 450 | } 451 | if ft.Kind() == reflect.Map && (recursive || filters.Contains(key)) { 452 | child := v.(map[string]interface{}) 453 | uri, found := child[REF_KEY] 454 | 455 | if found { 456 | resource, ok := getResourceFrom(uri.(string), filters, recursive) 457 | if ok { 458 | result[key] = resource 459 | } 460 | } 461 | } 462 | } 463 | 464 | return &result 465 | } 466 | 467 | func buildReferenceURI(t reflect.Value) string { 468 | var uri string 469 | 470 | if t.Kind() == reflect.Struct { 471 | collection := "" 472 | for i := 0; i < t.NumField(); i++ { 473 | f := t.Field(i) 474 | ft := t.Type().Field(i) 475 | 476 | if ft.Name == COLLECTION_KEY { 477 | collection = f.String() 478 | } else { 479 | objectId, ok := f.Interface().(ObjectId) 480 | if ok { 481 | base := ExpanderConfig.IdURIs[collection] 482 | uri = base+"/"+objectId.Hex() 483 | } 484 | } 485 | } 486 | } 487 | 488 | return uri 489 | } 490 | 491 | func isMongoDBRef(t reflect.Value) bool { 492 | mongoEnabled := ExpanderConfig.UsingMongo && len(ExpanderConfig.IdURIs) > 0 493 | 494 | if !mongoEnabled { 495 | return false 496 | } 497 | 498 | if t.Kind() == reflect.Struct { 499 | if t.NumField() != 3 { 500 | return false 501 | } 502 | 503 | for i := 0; i < t.NumField(); i++ { 504 | f := t.Field(i) 505 | 506 | if f.CanInterface() { 507 | _, ok := f.Interface().(ObjectId) 508 | if ok { 509 | return true 510 | } 511 | } 512 | } 513 | } 514 | 515 | return false 516 | } 517 | 518 | func isRefKey(ft reflect.StructField) bool { 519 | tag := strings.Split(ft.Tag.Get("json"), ",")[0] 520 | return ft.Name == REF_KEY || tag == REF_KEY 521 | } 522 | 523 | func isReference(t reflect.Value) bool { 524 | if t.Kind() == reflect.Struct { 525 | for i := 0; i < t.NumField(); i++ { 526 | ft := t.Type().Field(i) 527 | 528 | if isRefKey(ft) && t.NumField() > 1 { // at least relation & ref should be given 529 | return true 530 | } 531 | } 532 | } 533 | 534 | return false 535 | } 536 | 537 | func hasReference(m map[string]interface{}) bool { 538 | for _, v := range m { 539 | ft := reflect.TypeOf(v) 540 | 541 | if ft != nil && ft.Kind() == reflect.Map { 542 | child := v.(map[string]interface{}) 543 | _, ok := child[REF_KEY] 544 | 545 | if ok { 546 | return true 547 | } 548 | 549 | return hasReference(child) 550 | } 551 | } 552 | 553 | return false 554 | } 555 | 556 | func getReferenceURI(t reflect.Value) string { 557 | if t.Kind() == reflect.Struct { 558 | for i := 0; i < t.NumField(); i++ { 559 | ft := t.Type().Field(i) 560 | 561 | if isRefKey(ft) { 562 | return t.Field(i).String() 563 | } 564 | } 565 | } 566 | 567 | return "" 568 | } 569 | 570 | var makeGetCall = func(uri *url.URL) string { 571 | if !httpClientIsInitialized { 572 | initializerMutex.Lock() 573 | if !initializingHttpClient { 574 | initializingHttpClient = true 575 | Init() 576 | } 577 | initializerMutex.Unlock() 578 | } 579 | 580 | response, err := client.Get(uri.String()) 581 | if err != nil { 582 | fmt.Println(err) 583 | return "" 584 | } 585 | 586 | defer response.Body.Close() 587 | contents, err := ioutil.ReadAll(response.Body) 588 | 589 | if err != nil { 590 | fmt.Println("Error while reading content of response body. It was: ", err) 591 | } 592 | 593 | return string(contents) 594 | } 595 | 596 | var makeGetCallAndAddToCache = func(uri *url.URL) string { 597 | valueToReturn := makeGetCall(uri) 598 | 599 | var responseMap map[string]interface{} 600 | err := json.Unmarshal([]byte(valueToReturn), &responseMap) 601 | 602 | _, ok := responseMap["error"] 603 | if err != nil || ok { 604 | return "" 605 | } 606 | 607 | cacheEntry := CacheEntry{ 608 | Timestamp: time.Now().Unix(), 609 | Data: valueToReturn, 610 | } 611 | CacheMutex.Lock() 612 | Cache.Add(uri.String(), cacheEntry) 613 | CacheMutex.Unlock() 614 | return valueToReturn 615 | } 616 | 617 | 618 | var getContentFrom = func(uri *url.URL) string { 619 | if ExpanderConfig.UsingCache { 620 | CacheMutex.Lock() 621 | value, ok := Cache.Get(uri.String()) 622 | CacheMutex.Unlock() 623 | if !ok { 624 | //no data found in cache 625 | return makeGetCallAndAddToCache(uri) 626 | } 627 | 628 | cachedData := value.(CacheEntry) 629 | nowInMillis := time.Now().Unix() 630 | 631 | if nowInMillis-cachedData.Timestamp > ExpanderConfig.CacheExpInSeconds { 632 | //data older then Expiration 633 | CacheMutex.Lock() 634 | Cache.Remove(uri.String()) 635 | CacheMutex.Unlock() 636 | return makeGetCallAndAddToCache(uri) 637 | } 638 | 639 | return cachedData.Data 640 | } 641 | 642 | return makeGetCall(uri) 643 | } 644 | 645 | func validateFilterFormat(filter string) bool { 646 | runes := []rune(filter) 647 | 648 | var bracketCounter = 0 649 | 650 | for i := range runes { 651 | if runes[i] == '(' { 652 | bracketCounter++ 653 | }else if runes[i] == ')' { 654 | bracketCounter-- 655 | if bracketCounter < 0 { 656 | return false 657 | } 658 | } 659 | } 660 | return bracketCounter == 0 661 | 662 | } 663 | 664 | func buildFilterTree(statement string) ([]Filter, int) { 665 | var result []Filter 666 | const comma uint8 = ',' 667 | const openBracket uint8 = '(' 668 | const closeBracket uint8 = ')' 669 | 670 | if statement == "*" { 671 | return result, -1 672 | } 673 | 674 | statement = strings.Replace(statement, " ", "", -1) 675 | if len(statement) == 0 { 676 | return result, -1 677 | } 678 | 679 | indexAfterSeparation := 0 680 | closeIndex := 0 681 | 682 | for i := 0; i < len(statement); i++ { 683 | switch statement[i] { 684 | case openBracket: 685 | filter := Filter{Value: string(statement[indexAfterSeparation:i])} 686 | filter.Children, closeIndex = buildFilterTree(statement[i+1:]) 687 | result = append(result, filter) 688 | i = i+closeIndex 689 | indexAfterSeparation = i+1 690 | closeIndex = indexAfterSeparation 691 | case comma: 692 | filter := Filter{Value: string(statement[indexAfterSeparation:i])} 693 | if filter.Value != "" { 694 | result = append(result, filter) 695 | } 696 | indexAfterSeparation = i+1 697 | case closeBracket: 698 | filter := Filter{Value: string(statement[indexAfterSeparation:i])} 699 | if filter.Value != "" { 700 | result = append(result, filter) 701 | } 702 | 703 | return result, i+1 704 | } 705 | } 706 | 707 | if indexAfterSeparation > closeIndex { 708 | result = append(result, Filter{Value: string(statement[indexAfterSeparation:])}) 709 | } 710 | 711 | if indexAfterSeparation == 0 { 712 | result = append(result, Filter{Value: statement}) 713 | } 714 | 715 | return result, -1 716 | } 717 | -------------------------------------------------------------------------------- /expander/expander_test.go: -------------------------------------------------------------------------------- 1 | package expander 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | . "github.com/smartystreets/goconvey/convey" 7 | "net/url" 8 | "reflect" 9 | "strconv" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestExpander(t *testing.T) { 15 | 16 | Convey("It should walk the given object and identify it's type:", t, func() { 17 | Convey("Walking the type should return empty key-values if the object is nil", func() { 18 | result := Expand(nil, "", "") 19 | 20 | So(result, ShouldBeEmpty) 21 | }) 22 | 23 | Convey("Walking the type should return a map of all the visible simple key-values that user defines if expand is *", func() { 24 | expectedMap := make(map[string]interface{}) 25 | expectedMap["S"] = "bar" 26 | expectedMap["B"] = false 27 | expectedMap["I"] = -1 28 | expectedMap["F"] = 1.1 29 | expectedMap["UI"] = 1 30 | 31 | singleLevel := SimpleSingleLevel{S: "bar", B: false, I: -1, F: 1.1, UI: 1} 32 | result := Expand(singleLevel, "*", "") 33 | 34 | So(result["S"], ShouldEqual, expectedMap["S"]) 35 | So(result["B"], ShouldEqual, expectedMap["B"]) 36 | So(result["I"], ShouldEqual, expectedMap["I"]) 37 | So(result["F"], ShouldEqual, expectedMap["F"]) 38 | So(result["UI"], ShouldEqual, expectedMap["UI"]) 39 | }) 40 | 41 | Convey("Walking the type should return a map of all the visible simple key-values that user defines if expand is *", func() { 42 | simpleWithTime := SimpleWithTime{Name: "foo", Time: time.Now()} 43 | expectedMap := make(map[string]string) 44 | expectedMap["Name"] = simpleWithTime.Name 45 | t, _ := simpleWithTime.Time.MarshalJSON() 46 | time, _ := strconv.Unquote(string(t)) 47 | expectedMap["Time"] = time 48 | 49 | result := Expand(simpleWithTime, "*", "") 50 | 51 | So(result["Name"], ShouldEqual, expectedMap["Name"]) 52 | So(result["Time"], ShouldEqual, expectedMap["Time"]) 53 | }) 54 | 55 | Convey("Walking the type should assume expansion is * if no expansion parameter is given and return all the simple key-values that user defines", func() { 56 | expectedMap := make(map[string]interface{}) 57 | expectedMap["S"] = "bar" 58 | expectedMap["B"] = false 59 | expectedMap["I"] = -1 60 | expectedMap["F"] = 1.1 61 | expectedMap["UI"] = 1 62 | 63 | singleLevel := SimpleSingleLevel{S: "bar", B: false, I: -1, F: 1.1, UI: 1} 64 | result := Expand(singleLevel, "*", "") 65 | 66 | So(result["S"], ShouldEqual, expectedMap["S"]) 67 | So(result["B"], ShouldEqual, expectedMap["B"]) 68 | So(result["I"], ShouldEqual, expectedMap["I"]) 69 | So(result["F"], ShouldEqual, expectedMap["F"]) 70 | So(result["UI"], ShouldEqual, expectedMap["UI"]) 71 | }) 72 | 73 | Convey("Walking the type should return a map of all the simple with nested key-values that user defines if expand is *", func() { 74 | expectedMsb := map[string]bool{"key1": true, "key2": false} 75 | expectedMap := make(map[string]interface{}) 76 | expectedMap["SI"] = []int{1, 2} 77 | expectedMap["MSB"] = expectedMsb 78 | 79 | singleMultiLevel := SimpleMultiLevel{expectedMap["SI"].([]int), expectedMap["MSB"].(map[string]bool)} 80 | result := Expand(singleMultiLevel, "*", "") 81 | 82 | So(result["SI"], ShouldContain, 1) 83 | So(result["SI"], ShouldContain, 2) 84 | 85 | msb := result["MSB"].(map[string]interface{}) 86 | for k, v := range msb { 87 | key := fmt.Sprintf("%v", k) 88 | So(v, ShouldEqual, expectedMsb[key]) 89 | } 90 | }) 91 | 92 | Convey("Walking the type should return a map of all the complex key-values that user defines if expand is *", func() { 93 | simpleMap := make(map[string]interface{}) 94 | simpleMap["S"] = "bar" 95 | simpleMap["B"] = false 96 | simpleMap["I"] = -1 97 | simpleMap["F"] = 1.1 98 | simpleMap["UI"] = 1 99 | 100 | expectedMap := make(map[string]interface{}) 101 | expectedMap["SSL"] = simpleMap 102 | expectedMap["S"] = "a string" 103 | 104 | singleLevel := SimpleSingleLevel{S: "bar", B: false, I: -1, F: 1.1, UI: 1} 105 | complexSingleLevel := ComplexSingleLevel{S: expectedMap["S"].(string), SSL: singleLevel} 106 | 107 | result := Expand(complexSingleLevel, "*", "") 108 | ssl := result["SSL"].(map[string]interface{}) 109 | 110 | So(result["S"], ShouldEqual, expectedMap["S"]) 111 | So(ssl["S"], ShouldEqual, simpleMap["S"]) 112 | So(ssl["B"], ShouldEqual, simpleMap["B"]) 113 | So(ssl["I"], ShouldEqual, simpleMap["I"]) 114 | So(ssl["F"], ShouldEqual, simpleMap["F"]) 115 | So(ssl["UI"], ShouldEqual, simpleMap["UI"]) 116 | }) 117 | }) 118 | 119 | Convey("It should create a modification tree:", t, func() { 120 | Convey("Building a modification tree should be an empty expansion list when the expansion is *", func() { 121 | expansion := "*" 122 | result, _ := buildFilterTree(expansion) 123 | 124 | So(result, ShouldBeEmpty) 125 | }) 126 | 127 | Convey("Building a modification tree should be an empty expansion list when the expansion is not specified", func() { 128 | expansion := "" 129 | result, _ := buildFilterTree(expansion) 130 | 131 | So(result, ShouldBeEmpty) 132 | }) 133 | 134 | Convey("Building a modification tree should be a list of single field when the expansion specifies only one", func() { 135 | expansion := "A" 136 | 137 | result, _ := buildFilterTree(expansion) 138 | 139 | So(len(result), ShouldEqual, 1) 140 | So(result[0].Value, ShouldEqual, "A") 141 | }) 142 | 143 | Convey("Building a modification tree should be a list of all fields when the expansion specifies them", func() { 144 | expansion := "A, B" 145 | 146 | result, _ := buildFilterTree(expansion) 147 | 148 | So(len(result), ShouldEqual, 2) 149 | So(result[0].Value, ShouldEqual, "A") 150 | So(result[1].Value, ShouldEqual, "B") 151 | }) 152 | 153 | Convey("Building a modification tree should be a list of all nested fields when the expansion specifies them", func() { 154 | expansion := "A, B(C, D)" 155 | 156 | result, _ := buildFilterTree(expansion) 157 | 158 | So(len(result), ShouldEqual, 2) 159 | So(result[0].Value, ShouldEqual, "A") 160 | So(result[1].Value, ShouldEqual, "B") 161 | So(len(result[1].Children), ShouldEqual, 2) 162 | So(result[1].Children[0].Value, ShouldEqual, "C") 163 | So(result[1].Children[1].Value, ShouldEqual, "D") 164 | }) 165 | 166 | Convey("Building a modification tree should be a list of all nested fields and more when the expansion specifies them", func() { 167 | expansion := "A, B(C, D), E" 168 | 169 | result, _ := buildFilterTree(expansion) 170 | 171 | So(len(result), ShouldEqual, 3) 172 | So(result[0].Value, ShouldEqual, "A") 173 | So(result[1].Value, ShouldEqual, "B") 174 | So(result[2].Value, ShouldEqual, "E") 175 | So(len(result[1].Children), ShouldEqual, 2) 176 | So(result[1].Children[0].Value, ShouldEqual, "C") 177 | So(result[1].Children[1].Value, ShouldEqual, "D") 178 | }) 179 | 180 | Convey("Building a modification tree should be a list of all deeply-nested fields when the expansion specifies them", func() { 181 | expansion := "A, B(C(D, E), F), G" 182 | 183 | result, _ := buildFilterTree(expansion) 184 | 185 | So(len(result), ShouldEqual, 3) 186 | So(result[0].Value, ShouldEqual, "A") 187 | So(result[1].Value, ShouldEqual, "B") 188 | So(result[2].Value, ShouldEqual, "G") 189 | So(len(result[1].Children), ShouldEqual, 2) 190 | So(result[1].Children[0].Value, ShouldEqual, "C") 191 | So(result[1].Children[1].Value, ShouldEqual, "F") 192 | So(len(result[1].Children[0].Children), ShouldEqual, 2) 193 | So(result[1].Children[0].Children[0].Value, ShouldEqual, "D") 194 | So(result[1].Children[0].Children[1].Value, ShouldEqual, "E") 195 | }) 196 | 197 | Convey("Building a modification tree should be a list of all confusingly deeply-nested fields when the expansion specifies them", func() { 198 | expansion := "A(B(C(D))), E" 199 | 200 | result, _ := buildFilterTree(expansion) 201 | 202 | So(len(result), ShouldEqual, 2) 203 | So(result[0].Value, ShouldEqual, "A") 204 | So(result[1].Value, ShouldEqual, "E") 205 | So(len(result[0].Children), ShouldEqual, 1) 206 | So(result[0].Children[0].Value, ShouldEqual, "B") 207 | So(len(result[0].Children[0].Children), ShouldEqual, 1) 208 | So(result[0].Children[0].Children[0].Value, ShouldEqual, "C") 209 | So(len(result[0].Children[0].Children[0].Children), ShouldEqual, 1) 210 | So(result[0].Children[0].Children[0].Children[0].Value, ShouldEqual, "D") 211 | }) 212 | 213 | Convey("Building a modification tree should be a list of all nested fields when the expansion specifies only nested ones", func() { 214 | expansion := "A(B(C))" 215 | 216 | result, _ := buildFilterTree(expansion) 217 | 218 | So(len(result), ShouldEqual, 1) 219 | So(result[0].Value, ShouldEqual, "A") 220 | So(len(result[0].Children), ShouldEqual, 1) 221 | So(result[0].Children[0].Value, ShouldEqual, "B") 222 | So(len(result[0].Children[0].Children), ShouldEqual, 1) 223 | So(result[0].Children[0].Children[0].Value, ShouldEqual, "C") 224 | }) 225 | }) 226 | 227 | Convey("It should filter out the fields based on the given modification tree:", t, func() { 228 | Convey("Filtering should return an empty map when no Data is given", func() { 229 | filters := Filters{} 230 | result := walkByFilter(nil, filters) 231 | 232 | So(result, ShouldBeEmpty) 233 | }) 234 | 235 | Convey("Filtering should return a map with only selected fields on simple objects based on the modification tree", func() { 236 | singleLevel := map[string]interface{}{"S": "bar", "B": false, "I": -1, "F": 1.1, "UI": 1} 237 | filters := Filters{} 238 | filters = append(filters, Filter{Value: "S"}) 239 | filters = append(filters, Filter{Value: "I"}) 240 | 241 | result := walkByFilter(singleLevel, filters) 242 | 243 | So(result["S"], ShouldEqual, singleLevel["S"]) 244 | So(result["I"], ShouldEqual, singleLevel["I"]) 245 | So(result["B"], ShouldBeEmpty) 246 | So(result["F"], ShouldBeEmpty) 247 | So(result["UI"], ShouldBeEmpty) 248 | }) 249 | 250 | Convey("Filtering should return a map with only selected fields on multilevel single objects based on the modification tree", func() { 251 | expectedMsb := map[string]interface{}{"key1": true, "key2": false} 252 | expectedMap := make(map[string]interface{}) 253 | expectedMap["SI"] = []int{1, 2} 254 | expectedMap["MSB"] = expectedMsb 255 | 256 | child := Filter{Value: "key1"} 257 | parent := Filter{Value: "MSB", Children: []Filter{child}} 258 | filters := Filters{} 259 | filters = append(filters, parent) 260 | 261 | result := walkByFilter(expectedMap, filters) 262 | msb := result["MSB"].(map[string]interface{}) 263 | 264 | So(len(msb), ShouldEqual, 1) 265 | for k, v := range msb { 266 | key := fmt.Sprintf("%v", k) 267 | So(v, ShouldEqual, expectedMsb[key]) 268 | } 269 | }) 270 | 271 | Convey("Filtering should return a map with empty list on multilevel single objects if empty list given", func() { 272 | expectedMap := make(map[string]interface{}) 273 | expectedMap["SI"] = []int{} 274 | filters := Filters{} 275 | 276 | result := walkByFilter(expectedMap, filters) 277 | si := result["SI"].([]int) 278 | 279 | So(len(si), ShouldEqual, 0) 280 | }) 281 | 282 | Convey("Filtering should return a map with only selected fields on simple-multilevel objects based on the modification tree", func() { 283 | expectedMap := make(map[string]interface{}) 284 | expectedMap["S"] = "a string" 285 | expectedChildren := make([]map[string]interface{}, 0) 286 | expectedChildren = append(expectedChildren, map[string]interface{}{ 287 | "key1": "value1", 288 | "key2": 0, 289 | }) 290 | expectedChildren = append(expectedChildren, map[string]interface{}{ 291 | "key1": "value2", 292 | "key2": 1, 293 | }) 294 | expectedMap["Children"] = expectedChildren 295 | 296 | parent := Filter{Value: "Children", Children: Filters{Filter{Value: "key2"}}} 297 | filters := Filters{} 298 | filters = append(filters, Filter{Value: "S"}) 299 | filters = append(filters, parent) 300 | 301 | result := walkByFilter(expectedMap, filters) 302 | children := result["Children"].([]map[string]interface{}) 303 | 304 | So(result["S"], ShouldEqual, expectedMap["S"]) 305 | for i, v := range children { 306 | So(v["key1"], ShouldBeEmpty) 307 | So(v["key2"], ShouldEqual, i) 308 | } 309 | }) 310 | 311 | Convey("Filtering should return a map with only selected fields on complex objects based on the modification tree", func() { 312 | simpleMap := make(map[string]interface{}) 313 | simpleMap["S"] = "bar" 314 | simpleMap["B"] = false 315 | simpleMap["I"] = -1 316 | simpleMap["F"] = 1.1 317 | simpleMap["UI"] = 1 318 | 319 | expectedMap := make(map[string]interface{}) 320 | expectedMap["SSL"] = simpleMap 321 | expectedMap["S"] = "a string" 322 | 323 | child1 := Filter{Value: "B"} 324 | child2 := Filter{Value: "F"} 325 | parent := Filter{Value: "SSL", Children: Filters{child1, child2}} 326 | filters := Filters{} 327 | filters = append(filters, Filter{Value: "S"}) 328 | filters = append(filters, parent) 329 | 330 | result := walkByFilter(expectedMap, filters) 331 | ssl := result["SSL"].(map[string]interface{}) 332 | 333 | So(result["S"], ShouldEqual, expectedMap["S"]) 334 | So(len(ssl), ShouldEqual, 2) 335 | for k, v := range ssl { 336 | key := fmt.Sprintf("%v", k) 337 | So(v, ShouldEqual, simpleMap[key]) 338 | } 339 | }) 340 | 341 | }) 342 | 343 | Convey("It should filter out the fields based on the given modification tree during expansion:", t, func() { 344 | Convey("Filtering should return the full map when no Filters is given", func() { 345 | singleLevel := SimpleSingleLevel{S: "bar", B: false, I: -1, F: 1.1, UI: 1} 346 | 347 | result := Expand(singleLevel, "", "") 348 | 349 | So(result["S"], ShouldEqual, singleLevel.S) 350 | }) 351 | 352 | Convey("Filtering should return the filtered fields in simple object as map when first-level Filters given", func() { 353 | singleLevel := SimpleSingleLevel{S: "bar", B: false, I: -1, F: 1.1, UI: 1} 354 | 355 | result := Expand(singleLevel, "", "S, I") 356 | 357 | So(result["S"], ShouldEqual, singleLevel.S) 358 | So(result["I"], ShouldEqual, singleLevel.I) 359 | So(result["B"], ShouldBeEmpty) 360 | So(result["F"], ShouldBeEmpty) 361 | So(result["UI"], ShouldBeEmpty) 362 | }) 363 | 364 | Convey("Filtering should return the filtered fields in complex object as map when multi-level Filters given", func() { 365 | simpleMap := make(map[string]interface{}) 366 | simpleMap["S"] = "bar" 367 | simpleMap["B"] = false 368 | simpleMap["I"] = -1 369 | simpleMap["F"] = 1.1 370 | simpleMap["UI"] = 1 371 | 372 | expectedMap := make(map[string]interface{}) 373 | expectedMap["SSL"] = simpleMap 374 | expectedMap["S"] = "a string" 375 | 376 | singleLevel := SimpleSingleLevel{S: "bar", B: false, I: -1, F: 1.1, UI: 1} 377 | complexSingleLevel := ComplexSingleLevel{S: expectedMap["S"].(string), SSL: singleLevel} 378 | 379 | result := Expand(complexSingleLevel, "", "S,SSL(B, F, UI)") 380 | ssl := result["SSL"].(map[string]interface{}) 381 | 382 | So(result["S"], ShouldEqual, complexSingleLevel.S) 383 | So(len(ssl), ShouldEqual, 3) 384 | for k, v := range ssl { 385 | key := fmt.Sprintf("%v", k) 386 | So(v, ShouldEqual, simpleMap[key]) 387 | } 388 | }) 389 | }) 390 | 391 | Convey("It should identify if given field is a reference field:", t, func() { 392 | Convey("Identifying should return false when field is not a struct", func() { 393 | info := Info{"A name", 100} 394 | v := reflect.ValueOf(info) 395 | 396 | result := isReference(v.Field(0)) 397 | 398 | So(result, ShouldBeFalse) 399 | }) 400 | 401 | Convey("Identifying should return false when field is not a hypermedia link", func() { 402 | singleLevel := SimpleSingleLevel{S: "bar", B: false, I: -1, F: 1.1, UI: 1} 403 | complexSingleLevel := ComplexSingleLevel{S: "something", SSL: singleLevel} 404 | 405 | v := reflect.ValueOf(complexSingleLevel) 406 | 407 | result := isReference(v.Field(0)) 408 | 409 | So(result, ShouldBeFalse) 410 | }) 411 | 412 | Convey("Identifying should return true when field is a hypermedia link", func() { 413 | singleLevel := SimpleSingleLevel{L: Link{Ref: "http://valid", Rel: "nothing", Verb: "GET"}} 414 | 415 | v := reflect.ValueOf(singleLevel.L) 416 | 417 | result := isReference(v) 418 | 419 | So(result, ShouldBeTrue) 420 | }) 421 | 422 | Convey("Identifying should return false when field doesn't have a hypermedia link", func() { 423 | m := map[string]interface{}{ 424 | "a_key": "something", 425 | } 426 | 427 | result := hasReference(m) 428 | 429 | So(result, ShouldBeFalse) 430 | }) 431 | 432 | Convey("Identifying should return true when field has a hypermedia link", func() { 433 | m := map[string]interface{}{ 434 | "a_key": "something", 435 | "a_link": map[string]interface{}{ 436 | "ref": "http://valid", 437 | "rel": "a-relation", 438 | "verb": "GET", 439 | }, 440 | } 441 | 442 | result := hasReference(m) 443 | 444 | So(result, ShouldBeTrue) 445 | }) 446 | 447 | Convey("Identifying should return true when nested field has a hypermedia link", func() { 448 | m := map[string]interface{}{ 449 | "a_key": "something", 450 | "another_key": map[string]interface{}{ 451 | "some-id": "333", 452 | "a_link": map[string]interface{}{ 453 | "ref": "http://valid", 454 | "rel": "a-relation", 455 | "verb": "GET", 456 | }, 457 | }, 458 | } 459 | 460 | result := hasReference(m) 461 | 462 | So(result, ShouldBeTrue) 463 | }) 464 | 465 | Convey("Identifying should return false when nested field doesn't have a hypermedia link", func() { 466 | m := map[string]interface{}{ 467 | "a_key": "something", 468 | "another_key": map[string]interface{}{ 469 | "some-id": "333", 470 | "another_type": map[string]interface{}{ 471 | "something": "yeap", 472 | }, 473 | }, 474 | } 475 | 476 | result := hasReference(m) 477 | 478 | So(result, ShouldBeFalse) 479 | }) 480 | }) 481 | 482 | Convey("It should find expansion URI if given field is a reference field:", t, func() { 483 | Convey("Identifying should return empty string when no ref field", func() { 484 | info := Info{"A name", 100} 485 | v := reflect.ValueOf(info) 486 | 487 | result := getReferenceURI(v) 488 | 489 | So(result, ShouldBeEmpty) 490 | }) 491 | 492 | Convey("Identifying should return full URI when field is a ref field", func() { 493 | singleLevel := SimpleSingleLevel{L: Link{Ref: "http://valid", Rel: "nothing", Verb: "GET"}} 494 | 495 | v := reflect.ValueOf(singleLevel.L) 496 | 497 | result := getReferenceURI(v) 498 | 499 | So(result, ShouldEqual, singleLevel.L.Ref) 500 | }) 501 | }) 502 | 503 | Convey("It should fetch the underlying data from the Mongo during expansion:", t, func() { 504 | Convey("Fetching should return the same value when Mongo flag is not set", func() { 505 | simple := SimpleWithDBRef{Name: "foo", Ref: DBRef{"a collection", "an id", "a database"}} 506 | 507 | result := Expand(simple, "*", "") 508 | mongoRef := result["Ref"].(map[string]interface{}) 509 | So(result["name"], ShouldEqual, simple.Name) 510 | So(mongoRef["Collection"], ShouldEqual, simple.Ref.Collection) 511 | So(mongoRef["Id"], ShouldEqual, simple.Ref.Id) 512 | So(mongoRef["Database"], ShouldEqual, simple.Ref.Database) 513 | }) 514 | 515 | Convey("Fetching should return the same value when Mongo flag is set to false", func() { 516 | simple := SimpleWithDBRef{Name: "foo", Ref: DBRef{"a collection", "an id", "a database"}} 517 | 518 | ExpanderConfig = Configuration{UsingMongo: false} 519 | result := Expand(simple, "*", "") 520 | mongoRef := result["Ref"].(map[string]interface{}) 521 | 522 | So(result["name"], ShouldEqual, simple.Name) 523 | So(mongoRef["Collection"], ShouldEqual, simple.Ref.Collection) 524 | So(mongoRef["Id"], ShouldEqual, simple.Ref.Id) 525 | So(mongoRef["Database"], ShouldEqual, simple.Ref.Database) 526 | }) 527 | 528 | Convey("Fetching should return the same value when Mongo flag is set to true without IdURIs", func() { 529 | simple := SimpleWithDBRef{Name: "foo", Ref: DBRef{"a collection", MongoId("123"), "a database"}} 530 | 531 | ExpanderConfig = Configuration{UsingMongo: true} 532 | result := Expand(simple, "*", "") 533 | mongoRef := result["Ref"].(map[string]interface{}) 534 | 535 | So(result["name"], ShouldEqual, simple.Name) 536 | So(mongoRef["Collection"], ShouldEqual, simple.Ref.Collection) 537 | So(mongoRef["Id"], ShouldEqual, simple.Ref.Id) 538 | So(mongoRef["Database"], ShouldEqual, simple.Ref.Database) 539 | }) 540 | 541 | Convey("Fetching should return the underlying value when Mongo flag is set to true with proper IdURIs", func() { 542 | simple := SimpleWithDBRef{Name: "foo", Ref: DBRef{"a collection", MongoId("123"), "a database"}} 543 | info := Info{"A name", 100} 544 | uris := map[string]string{simple.Ref.Collection: "http://some-uri/id"} 545 | 546 | ExpanderConfig = Configuration{UsingMongo: true, IdURIs: uris} 547 | mockedFn := getContentFrom 548 | getContentFrom = func(url *url.URL) string { 549 | result, _ := json.Marshal(info) 550 | return string(result) 551 | } 552 | 553 | result := Expand(simple, "*", "") 554 | mongoRef := result["Ref"].(map[string]interface{}) 555 | 556 | So(result["name"], ShouldEqual, simple.Name) 557 | So(mongoRef["Name"], ShouldEqual, info.Name) 558 | So(mongoRef["Age"], ShouldEqual, info.Age) 559 | 560 | getContentFrom = mockedFn 561 | }) 562 | 563 | Convey("Fetching should return a list of underlying values when Mongo flag is set to true with proper IdURIs", func() { 564 | simple := SimpleWithMultipleDBRefs{ 565 | Name: "foo", 566 | Refs: []DBRef{ 567 | {"a collection", MongoId("123"), "a database"}, 568 | {"another collection", MongoId("234"), "another database"}, 569 | }, 570 | } 571 | info := Info{"A name", 100} 572 | uris := map[string]string{ 573 | "a collection": "http://some-uri/id", 574 | "another collection": "http://some-other-uri/id", 575 | } 576 | 577 | ExpanderConfig = Configuration{UsingMongo: true, IdURIs: uris} 578 | mockedFn := getContentFrom 579 | getContentFrom = func(url *url.URL) string { 580 | result, _ := json.Marshal(info) 581 | return string(result) 582 | } 583 | 584 | result := Expand(simple, "*", "") 585 | mongoRef := result["Refs"].([]interface{}) 586 | child1 := mongoRef[0].(map[string]interface{}) 587 | child2 := mongoRef[1].(map[string]interface{}) 588 | 589 | So(result["Name"], ShouldEqual, simple.Name) 590 | So(child1["Name"], ShouldEqual, info.Name) 591 | So(child1["Age"], ShouldEqual, info.Age) 592 | So(child2["Name"], ShouldEqual, info.Name) 593 | So(child2["Age"], ShouldEqual, info.Age) 594 | 595 | getContentFrom = mockedFn 596 | }) 597 | 598 | }) 599 | 600 | Convey("It should detect invalid filters and return data untouched", t, func() { 601 | Convey("Open brackets should be handled as invalid filter and not expand", func() { 602 | singleLevel := SimpleSingleLevel{L: Link{Ref: "http://valid", Rel: "nothing", Verb: "GET"}} 603 | info := Info{"A name", 100} 604 | 605 | mockedFn := getContentFrom 606 | getContentFrom = func(url *url.URL) string { 607 | result, _ := json.Marshal(info) 608 | return string(result) 609 | } 610 | 611 | result := Expand(singleLevel, "*,((", "") 612 | actual := result["L"].(map[string]interface{}) 613 | 614 | //still same after expansion, because filter is invalid 615 | So(actual["ref"], ShouldEqual, singleLevel.L.Ref) 616 | 617 | getContentFrom = mockedFn 618 | }) 619 | 620 | Convey("open brackets shouldbe handled as invalid filter and not apply filter", func() { 621 | singleLevel := SimpleSingleLevel{S: "bar", B: false, I: -1, F: 1.1, UI: 1} 622 | 623 | result := Expand(singleLevel, "", "S, I,((") 624 | 625 | So(result["S"], ShouldEqual, singleLevel.S) 626 | So(result["I"], ShouldEqual, singleLevel.I) 627 | So(result["B"], ShouldEqual, singleLevel.B) 628 | So(result["F"], ShouldEqual, singleLevel.F) 629 | So(result["UI"], ShouldEqual, singleLevel.UI) 630 | }) 631 | }) 632 | 633 | Convey("It should fetch the underlying data from the URIs during expansion:", t, func() { 634 | Convey("Fetching should return the same value when non-URI data structure given", func() { 635 | singleLevel := SimpleSingleLevel{L: Link{Ref: "non-URI", Rel: "nothing", Verb: "GET"}} 636 | 637 | result := Expand(singleLevel, "*", "") 638 | actual := result["L"].(map[string]interface{}) 639 | 640 | So(actual["ref"], ShouldEqual, singleLevel.L.Ref) 641 | So(actual["rel"], ShouldEqual, singleLevel.L.Rel) 642 | So(actual["verb"], ShouldEqual, singleLevel.L.Verb) 643 | }) 644 | 645 | Convey("Fetching should return the same value when non-URI data structure given", func() { 646 | singleLevel := SimpleSingleLevel{L: Link{Ref: "non-URI", Rel: "nothing", Verb: "GET"}} 647 | 648 | result := Expand(singleLevel, "*", "") 649 | actual := result["L"].(map[string]interface{}) 650 | 651 | So(actual["ref"], ShouldEqual, singleLevel.L.Ref) 652 | So(actual["rel"], ShouldEqual, singleLevel.L.Rel) 653 | So(actual["verb"], ShouldEqual, singleLevel.L.Verb) 654 | }) 655 | 656 | Convey("Fetching should replace the value with expanded data structure when valid URI given", func() { 657 | singleLevel := SimpleSingleLevel{L: Link{Ref: "http://valid", Rel: "nothing", Verb: "GET"}} 658 | info := Info{"A name", 100} 659 | 660 | mockedFn := getContentFrom 661 | getContentFrom = func(url *url.URL) string { 662 | result, _ := json.Marshal(info) 663 | return string(result) 664 | } 665 | 666 | result := Expand(singleLevel, "*", "") 667 | actual := result["L"].(map[string]interface{}) 668 | 669 | So(actual["Name"], ShouldEqual, info.Name) 670 | So(actual["Age"], ShouldEqual, info.Age) 671 | 672 | getContentFrom = mockedFn 673 | }) 674 | 675 | Convey("Fetching should replace an array of values with expanded data structures when valid URIs given", func() { 676 | links := []Link{ 677 | Link{"http://valid1", "relation1", "GET"}, 678 | Link{"http://valid2", "relation2", "GET"}, 679 | } 680 | 681 | info := []Info{ 682 | Info{"A name", 100}, 683 | Info{"Another name", 200}, 684 | } 685 | 686 | mockedFn := getContentFrom 687 | index := 0 688 | getContentFrom = func(url *url.URL) string { 689 | result, _ := json.Marshal(info[index]) 690 | index = index+1 691 | return string(result) 692 | } 693 | 694 | simpleWithLinks := SimpleWithLinks{"something", links} 695 | 696 | result := Expand(simpleWithLinks, "*", "") 697 | members := result["Members"].([]interface{}) 698 | 699 | So(result["Name"], ShouldEqual, simpleWithLinks.Name) 700 | 701 | for i, v := range members { 702 | member := v.(map[string]interface{}) 703 | 704 | So(member["Name"], ShouldEqual, info[i].Name) 705 | So(member["Age"], ShouldEqual, info[i].Age) 706 | } 707 | 708 | getContentFrom = mockedFn 709 | }) 710 | 711 | Convey("Fetching should replace the value recursively with expanded data structure when valid URIs given", func() { 712 | singleLevel1 := SimpleSingleLevel{S: "one", L: Link{Ref: "http://valid1/ssl", Rel: "nothing1", Verb: "GET"}} 713 | singleLevel2 := SimpleSingleLevel{S: "two", L: Link{Ref: "http://valid2/info", Rel: "nothing2", Verb: "GET"}} 714 | info := Info{"A name", 100} 715 | 716 | mockedFn := getContentFrom 717 | index := 0 718 | getContentFrom = func(url *url.URL) string { 719 | var result []byte 720 | if index > 0 { 721 | result, _ = json.Marshal(info) 722 | return string(result) 723 | } 724 | result, _ = json.Marshal(singleLevel2) 725 | index = index+1 726 | return string(result) 727 | } 728 | 729 | result := Expand(singleLevel1, "*", "") 730 | parent := result["L"].(map[string]interface{}) 731 | child := parent["L"].(map[string]interface{}) 732 | 733 | So(result["S"], ShouldEqual, singleLevel1.S) 734 | So(parent["S"], ShouldEqual, singleLevel2.S) 735 | So(child["Name"], ShouldEqual, info.Name) 736 | So(child["Age"], ShouldEqual, info.Age) 737 | 738 | getContentFrom = mockedFn 739 | }) 740 | 741 | Convey("Expanding should replace the value recursively and filter the expanded data structure when valid URIs given", func() { 742 | singleLevel1 := SimpleSingleLevel{S: "one", L: Link{Ref: "http://valid1/ssl", Rel: "nothing1", Verb: "GET"}} 743 | singleLevel2 := SimpleSingleLevel{S: "two", L: Link{Ref: "http://valid2/info", Rel: "nothing2", Verb: "GET"}} 744 | 745 | mockedFn := getContentFrom 746 | getContentFrom = func(url *url.URL) string { 747 | var result []byte 748 | result, _ = json.Marshal(singleLevel2) 749 | return string(result) 750 | } 751 | 752 | result := Expand(singleLevel1, "L", "") 753 | parent := result["L"].(map[string]interface{}) 754 | child := parent["L"].(map[string]interface{}) 755 | 756 | So(result["S"], ShouldEqual, singleLevel1.S) 757 | So(parent["S"], ShouldEqual, singleLevel2.S) 758 | So(child["ref"], ShouldEqual, singleLevel2.L.Ref) 759 | So(child["rel"], ShouldEqual, singleLevel2.L.Rel) 760 | So(child["verb"], ShouldEqual, singleLevel2.L.Verb) 761 | 762 | getContentFrom = mockedFn 763 | }) 764 | 765 | Convey("Expanding should replace the value recursively and filter the expanded data structure when data contains a list of nested sub-types", func() { 766 | link1 := Link{Ref: "http://valid1/ssl", Rel: "nothing1", Verb: "GET"} 767 | link2 := Link{Ref: "http://valid2/ssl", Rel: "nothing2", Verb: "GET"} 768 | singleLevel := SimpleSingleLevel{S: "one", L: link1} 769 | info := Info{"A name", 100} 770 | simpleWithLinks := SimpleWithLinks{ 771 | Name: "lorem", 772 | Members: []Link{link1, link2}, 773 | } 774 | 775 | mockedFn := getContentFrom 776 | index := 0 777 | getContentFrom = func(url *url.URL) string { 778 | var result []byte 779 | index = index+1 780 | if index%2 == 0 { 781 | result, _ = json.Marshal(info) 782 | return string(result) 783 | } 784 | result, _ = json.Marshal(singleLevel) 785 | return string(result) 786 | } 787 | 788 | result := Expand(simpleWithLinks, "Members(L)", "Name,Members(S,L)") 789 | parent := result["Members"].([]interface{}) 790 | 791 | So(len(result), ShouldEqual, 2) 792 | 793 | child1 := parent[0].(map[string]interface{}) 794 | So(child1["S"], ShouldEqual, singleLevel.S) 795 | 796 | actualLink := child1["L"].(map[string]interface{}) 797 | So(actualLink["Name"], ShouldEqual, info.Name) 798 | 799 | getContentFrom = mockedFn 800 | }) 801 | 802 | 803 | Convey("Expanding array should return the data with the array as root item ", func() { 804 | 805 | Convey("Expanding should return the same value if there is no MongoDB reference", func() { 806 | 807 | expectedItem1 := Info{"A name", 100} 808 | expectedItem2 := Info{"B name", 100} 809 | expectedItem3 := Info{"C name", 100} 810 | expectedArray := []Info{expectedItem1, expectedItem2, expectedItem3} 811 | 812 | result := ExpandArray(expectedArray, "*", "") 813 | 814 | So(len(result), ShouldEqual, 3) 815 | result1 := result[0].(map[string]interface{}) 816 | result2 := result[1].(map[string]interface{}) 817 | result3 := result[2].(map[string]interface{}) 818 | So(result1["Name"], ShouldEqual, expectedItem1.Name) 819 | So(result2["Name"], ShouldEqual, expectedItem2.Name) 820 | So(result3["Name"], ShouldEqual, expectedItem3.Name) 821 | }) 822 | 823 | Convey("Expanding should return the filtered array if there is filter defined", func() { 824 | 825 | expectedItem1 := Info{"A name", 100} 826 | expectedItem2 := Info{"B name", 101} 827 | expectedItem3 := Info{"C name", 102} 828 | expectedArray := []Info{expectedItem1, expectedItem2, expectedItem3} 829 | 830 | result := ExpandArray(expectedArray, "*", "Age") 831 | 832 | So(len(result), ShouldEqual, 3) 833 | result1 := result[0].(map[string]interface{}) 834 | result2 := result[1].(map[string]interface{}) 835 | result3 := result[2].(map[string]interface{}) 836 | So(result1["Name"], ShouldBeNil) 837 | So(result2["Name"], ShouldBeNil) 838 | So(result3["Age"], ShouldNotBeNil) 839 | }) 840 | 841 | Convey("Expanding should return the underlying value of the root of the array items from Mongo", func() { 842 | item1 := DBRef{"a collection", MongoId("123"), "a database"} 843 | item2 := DBRef{"a collection", MongoId("456"), "a database"} 844 | items := []DBRef{item1, item2} 845 | 846 | info1 := Info{"A name", 100} 847 | info2 := Info{"B name", 100} 848 | infos := []Info{info1, info2} 849 | 850 | uris := map[string]string{item1.Collection: "http://some-uri/id"} 851 | 852 | ExpanderConfig = Configuration{UsingMongo: true, IdURIs: uris} 853 | mockedFn := getContentFrom 854 | getContentFrom = func(url *url.URL) string { 855 | fmt.Println(url) 856 | if url.Path == "/id/123" { 857 | result, _ := json.Marshal(info1) 858 | return string(result) 859 | } else { 860 | result, _ := json.Marshal(info2) 861 | return string(result) 862 | } 863 | } 864 | 865 | result := ExpandArray(items, "*", "") 866 | So(len(result), ShouldEqual, len(infos)) 867 | 868 | result1 := result[0].(map[string]interface{}) 869 | result2 := result[1].(map[string]interface{}) 870 | So(result1["Name"], ShouldEqual, info1.Name) 871 | So(result2["Name"], ShouldEqual, info2.Name) 872 | 873 | getContentFrom = mockedFn 874 | }) 875 | }) 876 | Convey("When caching is enabled, it should emit a second call to the same URI of a valid reference", func() { 877 | ExpanderConfig.UsingCache = true 878 | ExpanderConfig.CacheExpInSeconds = 86400 879 | Convey("Fetching a valid Reference first time should make GET call and replace the data correctly", func() { 880 | singleLevel := SimpleSingleLevel{L: Link{Ref: "http://valid", Rel: "nothing", Verb: "GET"}} 881 | info := Info{"A name", 100} 882 | 883 | mockedFn := getContentFrom 884 | makeGetCall = func(url *url.URL) string { 885 | result, _ := json.Marshal(info) 886 | return string(result) 887 | } 888 | 889 | result := Expand(singleLevel, "*", "") 890 | actual := result["L"].(map[string]interface{}) 891 | 892 | So(actual["Name"], ShouldEqual, info.Name) 893 | So(actual["Age"], ShouldEqual, info.Age) 894 | 895 | makeGetCall = mockedFn 896 | }) 897 | Convey("Fetching a valid Reference second time should NOT make GET call and replace the data correctly", func() { 898 | singleLevel := SimpleSingleLevel{L: Link{Ref: "http://valid", Rel: "nothing", Verb: "GET"}} 899 | info := Info{"A name", 100} 900 | 901 | mockedFn := getContentFrom 902 | makeGetCall = func(url *url.URL) string { 903 | //this should not be called, so return invalid data to make the test fail in case it is called: 904 | return "INVALID_DATA" 905 | } 906 | 907 | result := Expand(singleLevel, "*", "") 908 | actual := result["L"].(map[string]interface{}) 909 | 910 | So(actual["Name"], ShouldEqual, info.Name) 911 | So(actual["Age"], ShouldEqual, info.Age) 912 | 913 | makeGetCall = mockedFn 914 | }) 915 | Convey("Fetching Reference second/third/... time should make GET call when cached data is older then 24h and replace the data correctly", func() { 916 | uri := "http://valid" 917 | singleLevel := SimpleSingleLevel{L: Link{Ref: uri, Rel: "nothing", Verb: "GET"}} 918 | info := Info{"A name", 100} 919 | 920 | expiredTimestamp := time.Now().Unix() - (ExpanderConfig.CacheExpInSeconds + 1) 921 | invalidData := "INVALID_DATA" 922 | 923 | Cache.Remove(uri) 924 | Cache.Add(uri, CacheEntry{Timestamp: expiredTimestamp, Data: invalidData}) 925 | 926 | 927 | mockedFn := getContentFrom 928 | makeGetCall = func(url *url.URL) string { 929 | result, _ := json.Marshal(info) 930 | return string(result) 931 | } 932 | 933 | result := Expand(singleLevel, "*", "") 934 | actual := result["L"].(map[string]interface{}) 935 | 936 | So(actual["Name"], ShouldEqual, info.Name) 937 | So(actual["Age"], ShouldEqual, info.Age) 938 | 939 | makeGetCall = mockedFn 940 | }) 941 | }) 942 | ExpanderConfig.UsingCache = false 943 | }) 944 | } 945 | 946 | type Link struct { 947 | Ref string `json:"ref"` 948 | Rel string `json:"rel"` 949 | Verb string `json:"verb"` 950 | } 951 | 952 | type MongoId string 953 | 954 | func (m MongoId) Hex() string { 955 | return string(m) 956 | } 957 | 958 | type Info struct { 959 | Name string 960 | Age int 961 | } 962 | 963 | type SimpleWithLinks struct { 964 | Name string 965 | Members []Link 966 | } 967 | 968 | type SimpleWithDBRef struct { 969 | Name string `json:"name,omitempty"` 970 | Ref DBRef `json: "ref", bson: "ref"` 971 | } 972 | 973 | type SimpleWithTime struct { 974 | Name string 975 | Time time.Time 976 | } 977 | 978 | type SimpleWithMultipleDBRefs struct { 979 | Name string 980 | Refs []DBRef 981 | } 982 | 983 | type SimpleSingleLevel struct { 984 | S string 985 | B bool 986 | I int 987 | F float64 988 | UI uint 989 | // hidden bool 990 | L Link 991 | } 992 | 993 | type SimpleMultiLevel struct { 994 | SI []int 995 | MSB map[string]bool 996 | } 997 | 998 | type ComplexSingleLevel struct { 999 | SSL SimpleSingleLevel 1000 | S string 1001 | } 1002 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: wercker/golang@1.2.0 2 | 3 | # Build definition 4 | build: 5 | 6 | # The steps that will be executed on build 7 | steps: 8 | # Sets the go workspace and places you package 9 | # at the right place in the workspace tree 10 | - setup-go-workspace 11 | 12 | # Gets the dependencies 13 | - script: 14 | name: go get 15 | code: | 16 | cd $WERCKER_SOURCE_DIR 17 | go version 18 | go get -t github.com/smartystreets/goconvey 19 | go get -t github.com/golang/groupcache/lru 20 | 21 | # Test the project 22 | - script: 23 | name: go test 24 | code: | 25 | cd $WERCKER_SOURCE_DIR/expander 26 | go test -v 27 | --------------------------------------------------------------------------------