pax_global_header00006660000000000000000000000064131756771370014533gustar00rootroot0000000000000052 comment=e7b693844a6ff1798ca2a84d8481884ca81d191a api2go-1.0-RC4/000077500000000000000000000000001317567713700131425ustar00rootroot00000000000000api2go-1.0-RC4/.gitignore000066400000000000000000000004031317567713700151270ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test api2go-1.0-RC4/.travis.yml000066400000000000000000000020211317567713700152460ustar00rootroot00000000000000language: go go: - 1.7 - 1.8 - 1.9 - tip sudo: false install: - go get -t -d -v ./... - go get github.com/onsi/ginkgo/ginkgo - go get -u github.com/golang/lint/golint - go get -u github.com/modocache/gover - go get -u github.com/mattn/goveralls # optional dependencies - go get -u github.com/gin-gonic/gin - go get -u github.com/gorilla/mux - go get -u github.com/labstack/echo/... script: - ginkgo -r -cover --randomizeAllSpecs --randomizeSuites --failOnPending --trace --race --progress - ginkgo -tags=gorillamux -r --randomizeSuites --failOnPending --trace --race - ginkgo -tags=gingonic -r --randomizeSuites --failOnPending --trace --race - ginkgo -tags=echo -r --randomizeSuites --failOnPending --trace --race - rm examples/examples.coverprofile - bash scripts/fmtpolice - gover - goveralls -coverprofile=gover.coverprofile -repotoken gY90SprlNRGmSMl7MgybLreYa05wUXJTU notifications: webhooks: urls: - https://webhooks.gitter.im/e/9caacfa1bede5e900019 on_success: change api2go-1.0-RC4/LICENSE000066400000000000000000000020641317567713700141510ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 manyminds Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. api2go-1.0-RC4/README.md000066400000000000000000000553741317567713700144370ustar00rootroot00000000000000# api2go [![Join the chat at https://gitter.im/manyminds/api2go](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/manyminds/api2go?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![GoDoc](https://godoc.org/github.com/manyminds/api2go?status.svg)](https://godoc.org/github.com/manyminds/api2go) [![Build Status](https://travis-ci.org/manyminds/api2go.svg?branch=master)](https://travis-ci.org/manyminds/api2go) [![Coverage Status](https://coveralls.io/repos/github/manyminds/api2go/badge.svg?branch=master)](https://coveralls.io/github/manyminds/api2go?branch=master) [![Go Report Card](https://goreportcard.com/badge/manyminds/api2go)](https://goreportcard.com/report/manyminds/api2go) A [JSON API](http://jsonapi.org) Implementation for Go, to be used e.g. as server for [Ember Data](https://github.com/emberjs/data). ## TOC - [Installation](#installation) - [Basic functionality](#basic-functionality) - [Examples](#examples) - [Interfaces to implement](#interfaces-to-implement) - [Responder](#responder) - [EntityNamer](#entitynamer) - [MarshalIdentifier](#marshalidentifier) - [UnmarshalIdentifier](#unmarshalidentifier) - [Marshalling with References to other structs](#marshalling-with-references-to-other-structs) - [Unmarshalling with references to other structs](#unmarshalling-with-references-to-other-structs) - [Manual marshalling / unmarshalling](#manual-marshalling--unmarshalling) - [SQL Null-Types](#sql-null-types) - [Using api2go with the gin framework](#using-api2go-with-the-gin-framework) - [Building a REST API](#building-a-rest-api) - [Query Params](#query-params) - [Using Pagination](#using-pagination) - [Fetching related IDs](#fetching-related-ids) - [Fetching related resources](#fetching-related-resources) - [Using middleware](#using-middleware) - [Dynamic URL Handling](#dynamic-url-handling) - [Tests](#tests) # Installation For the complete api2go package use: ```go go get github.com/manyminds/api2go ``` If you only need marshalling and/or unmarshalling: ``` go get github.com/manyminds/api2go/jsonapi ``` ## Basic functionality Api2go will Marshal/Unmarshal exactly like the internal `json` package from Go with one addition: It will decorate the Marshalled json with jsonapi meta objects. Jsonapi wraps the payload inside an `attributes` object. The rest is just Meta-Data which will be generated by api2go. So let's take this basic example: ```go type Article struct { ID string Title string `json:"title"` } ``` Would `json.Marshal` into this Json: ```json { "ID": "Some-ID", "title": "the title" } ``` For api2go, you have to ignore tag the `ID` field and then the result could be something like this: ```json { "type": "articles", "id": "1", "attributes": { "title": "Rails is Omakase" }, "relationships": { "author": { "links": { "self": "/articles/1/relationships/author", "related": "/articles/1/author" }, "data": { "type": "people", "id": "9" } } } } ``` All the additional information is retrieved by implementing some interfaces. ## Examples - Basic Examples can be found [here](https://github.com/manyminds/api2go/blob/master/examples/crud_example.go). - For a more real life example implementation of api2go using [jinzhu/gorm](https://github.com/jinzhu/gorm) and [gin-gonic/gin](https://github.com/gin-gonic/gin) you can have a look at hnakamur's [repository](https://github.com/hnakamur/api2go-gorm-gin-crud-example) ## Interfaces to implement For the following query and result examples, imagine the following 2 structs which represent a posts and comments that belong with a has-many relation to the post. ```go type Post struct { ID int `json:"-"` // Ignore ID field because the ID is fetched via the // GetID() method and must not be inside the attributes object. Title string `json:"title"` Comments []Comment `json:"-"` // this will be ignored by the api2go marshaller CommentsIDs []int `json:"-"` // it's only useful for our internal relationship handling } type Comment struct { ID int `json:"-"` Text string `json:"text"` } ``` You must at least implement the [MarshalIdentifier](#marshalidentifier) interface, which is the one for marshalling/unmarshalling the primary `ID` of the struct that you want to marshal/unmarshal. This is because of the huge variety of types that you could use for the primary ID. For example a string, a UUID or a BSON Object for MongoDB etc... In the Post example struct, the `ID` field is ignored because api2go will use the `GetID` method that you implemented for your struct to fetch the ID of the struct. Every field inside a struct will be marshalled into the `attributes` object in the json. In our example, we just want to have the `Title` field there. Don't forget to name all your fields with the `json:"yourName"` tag. ### Responder ```go type Responder interface { Metadata() map[string]interface{} Result() interface{} StatusCode() int } ``` The Responder interface must be implemented if you are using our API. It contains everything that is needed for a response. You can see an example usage of it in our example project. ### EntityNamer ```go type EntityNamer interface { GetName() string } ``` EntityNamer is an optional interface. Normally, the name of a struct will be automatically generated in its plural form. For example if your struct has the type `Post`, its generated name is `posts`. And the url for the GET request for post with ID 1 would be `/posts/1`. If you implement the `GetName()` method and it returns `special-posts`, then this would be the name in the `type` field of the generated json and also the name for the generated routes. Currently, you must implement this interface, if you have a struct type that consists of multiple words and you want to use a **hyphenized** name. For example `UnicornPost`. Our default Jsonifier would then generate the name `unicornPosts`. But if you want the [recommended](http://jsonapi.org/recommendations/#naming) name, you have to implement `GetName` ```go func (s UnicornPost) GetName() string { return "unicorn-posts" } ``` ### MarshalIdentifier ```go type MarshalIdentifier interface { GetID() string } ``` Implement this interface to marshal a struct. ### UnmarshalIdentifier ```go type UnmarshalIdentifier interface { SetID(string) error } ``` This is the corresponding interface to MarshalIdentifier. Implement this interface in order to unmarshal incoming json into a struct. ### Marshalling with References to other structs For relationships to work, there are 3 Interfaces that you can use: ```go type MarshalReferences interface { GetReferences() []Reference } // MarshalLinkedRelations must be implemented if there are references and the reference IDs should be included type MarshalLinkedRelations interface { MarshalReferences MarshalIdentifier GetReferencedIDs() []ReferenceID } // MarshalIncludedRelations must be implemented if referenced structs should be included type MarshalIncludedRelations interface { MarshalReferences MarshalIdentifier GetReferencedStructs() []MarshalIdentifier } ``` Implementing those interfaces is not mandatory and depends on your use cases. If your API has any relationships, you must at least implement `MarshalReferences` and `MarshalLinkedRelations`. `MarshalReferences` must be implemented in order for api2go to know which relations are possible for your struct. `MarshalLinkedRelations` must be implemented to retrieve the `IDs` of the relations that are connected to this struct. This method could also return an empty array, if there are currently no relations. This is why there is the `MarshalReferences` interface, so that api2go knows what is possible, even if nothing is referenced at the time. In addition to that, you can implement `MarshalIncludedRelations` which exports the complete referenced structs and embeds them in the json result inside the `included` object. **That way you can choose how you internally manage relations.** So, there are no limits regarding the use of ORMs. ### Unmarshalling with references to other structs Incoming jsons can also contain reference IDs. In order to unmarshal them correctly, you have to implement the following interfaces. If you only have to-one relationships, the `UnmarshalToOneRelations` interface is enough. ```go // UnmarshalToOneRelations must be implemented to unmarshal to-one relations type UnmarshalToOneRelations interface { SetToOneReferenceID(name, ID string) error } // UnmarshalToManyRelations must be implemented to unmarshal to-many relations type UnmarshalToManyRelations interface { SetToManyReferenceIDs(name string, IDs []string) error } ``` **If you need to know more about how to use the interfaces, look at our tests or at the example project.** ## Manual marshalling / unmarshalling Please keep in mind that this only works if you implemented the previously mentioned interfaces. Manual marshalling and unmarshalling makes sense, if you do not want to use our API that automatically generates all the necessary routes for you. You can directly use our sub-package `github.com/manyminds/api2go/jsonapi` ```go comment1 = Comment{ID: 1, Text: "First!"} comment2 = Comment{ID: 2, Text: "Second!"} post = Post{ID: 1, Title: "Foobar", Comments: []Comment{comment1, comment2}} json, err := jsonapi.Marshal(post) ``` will yield ```json { "data": [ { "id": "1", "type": "posts", "attributes": { "title": "Foobar" }, "relationships": { "comments": { "data": [ { "id": "1", "type": "comments" }, { "id": "2", "type": "comments" } ] } } } ], "included": [ { "id": "1", "type": "comments", "attributes": { "text": "First!" } }, { "id": "2", "type": "comments", "attributes": { "text": "Second!" } } ] } ``` You can also use `jsonapi.MarshalWithURLs` to automatically generate URLs for the rest endpoints that have a version and BaseURL prefix. This will generate the same routes that our API uses. This adds `self` and `related` fields for relations inside the `relationships` object. Recover the structure from above using. Keep in mind that Unmarshalling with included structs does not work yet. So Api2go cannot be used as a client yet. ```go var posts []Post err := jsonapi.Unmarshal(json, &posts) // posts[0] == Post{ID: 1, Title: "Foobar", CommentsIDs: []int{1, 2}} ``` ## SQL Null-Types When using a SQL Database it is most likely you want to use the special SQL-Types from the `database/sql` package. These are - sql.NullBool - sql.NullFloat64 - sql.NullInt64 - sql.NullString The Problem is, that they internally manage the `null` value behavior by using a custom struct. In order to Marshal und Unmarshal these values, it is required to implement the `json.Marshaller` and `json.Unmarshaller` interfaces of the go standard library. But you dont have to do this by yourself! There already is a library that did the work for you. We recommend that you use the types of this library: http://gopkg.in/guregu/null.v2/zero In order to use omitempty with those types, you need to specify them as pointers in your struct. ## Using api2go with the gin framework If you want to use api2go with [gin](https://github.com/gin-gonic/gin) you need to use a different router than the default one. Get the according adapter using: ```go get -tags=gingonic github.com/manyminds/api2go``` Currently the supported tags are: `gingonic`,`gorillamux`, or `echo`. After that you can bootstrap api2go the following way: ```go import ( "github.com/gin-gonic/gin" "github.com/manyminds/api2go" "github.com/manyminds/api2go/routing" "github.com/manyminds/api2go/examples/model" "github.com/manyminds/api2go/examples/resource" "github.com/manyminds/api2go/examples/storage" ) func main() { r := gin.Default() api := api2go.NewAPIWithRouting( "api", api2go.NewStaticResolver("/"), routing.Gin(r), ) userStorage := storage.NewUserStorage() chocStorage := storage.NewChocolateStorage() api.AddResource(model.User{}, resource.UserResource{ChocStorage: chocStorage, UserStorage: userStorage}) api.AddResource(model.Chocolate{}, resource.ChocolateResource{ChocStorage: chocStorage, UserStorage: userStorage}) r.GET("/ping", func(c *gin.Context) { c.String(200, "pong") }) r.Run(":8080") } ``` Keep in mind that you absolutely should map api2go under its own namespace to not get conflicts with your normal routes. If you need api2go with any different go framework, just send a PR with the according adapter :-) ## Building a REST API First, write an implementation of either `api2go.ResourceGetter`, `api2go.ResourceCreator`, `api2go.ResourceUpdater`, `api2go.ResourceDeleter`, or any combination of them. You can also write an implementation the `CRUD` interface which embed all of them. You have to implement at least one of these 4 methods: ```go type fixtureSource struct {} // FindOne returns an object by its ID // Possible success status code 200 func (s *fixtureSource) FindOne(ID string, r api2go.Request) (Responder, error) {} // Create a new object. Newly created object/struct must be in Responder. // Possible status codes are: // - 201 Created: Resource was created and needs to be returned // - 202 Accepted: Processing is delayed, return nothing // - 204 No Content: Resource created with a client generated ID, and no fields were modified by // the server func (s *fixtureSource) Create(obj interface{}, r api2go.Request) (Responder, err error) {} // Delete an object // Possible status codes are: // - 200 OK: Deletion was a success, returns meta information, currently not implemented! Do not use this // - 202 Accepted: Processing is delayed, return nothing // - 204 No Content: Deletion was successful, return nothing func (s *fixtureSource) Delete(id string, r api2go.Request) (Responder, err error) {} // Update an object // Possible status codes are: // - 200 OK: Update successful, however some field(s) were changed, returns updates source // - 202 Accepted: Processing is delayed, return nothing // - 204 No Content: Update was successful, no fields were changed by the server, return nothing func (s *fixtureSource) Update(obj interface{}, r api2go.Request) (Responder, err error) {} ``` If you want to return a jsonapi compatible error because something went wrong inside the CRUD methods, you can use our `HTTPError` struct, which can be created with `NewHTTPError`. This allows you to set the error status code and add as many information about the error as you like. See: [jsonapi error](http://jsonapi.org/format/#errors) To fetch all objects of a specific resource you can choose to implement one or both of the following interfaces: ```go type FindAll interface { // FindAll returns all objects FindAll(req Request) (Responder, error) } type PaginatedFindAll interface { PaginatedFindAll(req Request) (totalCount uint, response Responder, err error) } ``` `FindAll` returns everything. You could limit the results only by using Query Params which are described [here](#query-params) `PaginatedFindAll` can also use Query Params, but in addition to that it does not need to send all objects at once and can split up the result with pagination. You have to return the total number of found objects in order to let our API automatically generate pagination links. More about pagination is described [here](#using-pagination) You can then create an API: ```go api := api2go.NewAPI("v1") api.AddResource(Post{}, &PostsSource{}) http.ListenAndServe(":8080", api.Handler()) ``` Instead of `api2go.NewAPI` you can also use `api2go.NewAPIWithBaseURL("v1", "http://yourdomain.com")` to prefix all automatically generated routes with your domain and protocoll. This generates the standard endpoints: ``` OPTIONS /v1/posts OPTIONS /v1/posts/ GET /v1/posts POST /v1/posts GET /v1/posts/ PATCH /v1/posts/ DELETE /v1/posts/ GET /v1/posts//comments // fetch referenced comments of a post GET /v1/posts//relationships/comments // fetch IDs of the referenced comments only PATCH /v1/posts//relationships/comments // replace all related comments // These 2 routes are only created for to-many relations that implement EditToManyRelations interface POST /v1/posts//relationships/comments // Add a new comment reference, only for to-many relations DELETE /v1/posts//relationships/comments // Delete a comment reference, only for to-many relations ``` For the last two generated routes, it is necessary to implement the `jsonapi.EditToManyRelations` interface. ```go type EditToManyRelations interface { AddToManyIDs(name string, IDs []string) error DeleteToManyIDs(name string, IDs []string) error } ``` All PATCH, POST and DELETE routes do a `FindOne` and update the values/relations in the previously found struct. This struct will then be passed on to the `Update` method of a resource struct. So you get all these routes "for free" and just have to implement the `ResourceUpdater` `Update` method. ### Query Params To support all the features mentioned in the `Fetching Resources` section of Jsonapi: http://jsonapi.org/format/#fetching If you want to support any parameters mentioned there, you can access them in your Resource via the `api2go.Request` Parameter. This currently supports `QueryParams` which holds all query parameters as `map[string][]string` unfiltered. So you can use it for: * Filtering * Inclusion of Linked Resources * Sparse Fieldsets * Sorting * Aything else you want to do that is not in the official Jsonapi Spec ```go type fixtureSource struct {} func (s *fixtureSource) FindAll(req api2go.Request) (Responder, error) { for key, values range req.QueryParams { ... } ... } ``` If there are multiple values, you have to separate them with a comma. api2go automatically slices the values for you. ``` Example Request GET /people?fields=id,name,age req.QueryParams["fields"] contains values: ["id", "name", "age"] ``` ### Using Pagination Api2go can automatically generate the required links for pagination. Currently there are 2 combinations of query parameters supported: - page[number], page[size] - page[offset], page[limit] Pagination is optional. If you want to support pagination, you have to implement the `PaginatedFindAll` method in you resource struct. For an example, you best look into our example project. Example request ``` GET /v0/users?page[number]=2&page[size]=2 ``` would return a json with the top level links object ```json { "links": { "first": "http://localhost:31415/v0/users?page[number]=1&page[size]=2", "last": "http://localhost:31415/v0/users?page[number]=5&page[size]=2", "next": "http://localhost:31415/v0/users?page[number]=3&page[size]=2", "prev": "http://localhost:31415/v0/users?page[number]=1&page[size]=2" }, "data": [...] } ``` ### Fetching related IDs The IDs of a relationship can be fetched by following the `self` link of a relationship object in the `links` object of a result. For the posts and comments example you could use the following generated URL: ``` GET /v1/posts/1/relationships/comments ``` This would return all comments that are currently referenced by post with ID 1. For example: ```json { "links": { "self": "/v1/posts/1/relationships/comments", "related": "/v1/posts/1/comments" }, "data": [ { "type": "comments", "id": "1" }, { "type":"comments", "id": "2" } ] } ``` ### Fetching related resources Api2go always creates a `related` field for elements in the `relationships` object of the result. This is like it's specified on jsonapi.org. Post example: ```json { "data": [ { "id": "1", "type": "posts", "title": "Foobar", "relationships": { "comments": { "links": { "related": "/v1/posts/1/comments", "self": "/v1/posts/1/relationships/comments" }, "data": [ { "id": "1", "type": "comments" }, { "id": "2", "type": "comments" } ] } } } ] } ``` If a client requests this `related` url, the `FindAll` method of the comments resource will be called with a query parameter `postsID`. So if you implement the `FindAll` method, do not forget to check for all possible query Parameters. This means you have to check all your other structs and if it references the one for that you are implementing `FindAll`, check for the query Paramter and only return comments that belong to it. In this example, return the comments for the Post. ### Using middleware We provide a custom `APIContext` with a [context](https://godoc.org/context) implementation that you can use if you for example need to check if a user is properly authenticated before a request reaches the api2go routes. You can either use our struct or implement your own with the `APIContexter` interface ```go type APIContexter interface { context.Context Set(key string, value interface{}) Get(key string) (interface{}, bool) Reset() } ``` If you implemented your own `APIContexter`, don't forget to define a `APIContextAllocatorFunc` and set it with `func (api *API) SetContextAllocator(allocator APIContextAllocatorFunc)` But in most cases, this is not needed. To use a middleware, it is needed to implement our `type HandlerFunc func(APIContexter, http.ResponseWriter, *http.Request)`. A `HandlerFunc` can then be registered with `func (api *API) UseMiddleware(middleware ...HandlerFunc)`. You can either pass one or many middlewares that will be executed in order before any other api2go routes. Use this to set up database connections, user authentication and so on. ### Dynamic URL handling If you have different TLDs for one api, or want to use different domains in development and production, you can implement a custom URLResolver in api2go. There is a simple interface, which can be used if you get TLD information from the database, the server environment, or anything else that's not request dependant: ```go type URLResolver interface { GetBaseURL() string } ``` And a more complex one that also gets request information: ```go type RequestAwareURLResolver interface { URLResolver SetRequest(http.Request) } ``` For most use cases we provide a CallbackResolver which works on a per request basis and may fill your basic needs. This is particulary useful if you are using an nginx proxy which sets `X-Forwarded-For` headers. ```go resolver := NewCallbackResolver(func(r http.Request) string{}) api := NewApiWithMarshalling("v1", resolver, marshalers) ``` ## Tests ```sh go test ./... ginkgo -r # Alternative ginkgo watch -r -notify # Watch for changes ``` api2go-1.0-RC4/api.go000066400000000000000000001022501317567713700142420ustar00rootroot00000000000000package api2go import ( "encoding/json" "errors" "fmt" "io/ioutil" "log" "net/http" "net/url" "reflect" "regexp" "strconv" "strings" "github.com/manyminds/api2go/jsonapi" "github.com/manyminds/api2go/routing" ) const ( codeInvalidQueryFields = "API2GO_INVALID_FIELD_QUERY_PARAM" defaultContentTypHeader = "application/vnd.api+json" ) var ( queryPageRegex = regexp.MustCompile(`^page\[(\w+)\]$`) queryFieldsRegex = regexp.MustCompile(`^fields\[(\w+)\]$`) ) type information struct { prefix string resolver URLResolver } func (i information) GetBaseURL() string { return i.resolver.GetBaseURL() } func (i information) GetPrefix() string { return i.prefix } type paginationQueryParams struct { number, size, offset, limit string } func newPaginationQueryParams(r *http.Request) paginationQueryParams { var result paginationQueryParams queryParams := r.URL.Query() result.number = queryParams.Get("page[number]") result.size = queryParams.Get("page[size]") result.offset = queryParams.Get("page[offset]") result.limit = queryParams.Get("page[limit]") return result } func (p paginationQueryParams) isValid() bool { if p.number == "" && p.size == "" && p.offset == "" && p.limit == "" { return false } if p.number != "" && p.size != "" && p.offset == "" && p.limit == "" { return true } if p.number == "" && p.size == "" && p.offset != "" && p.limit != "" { return true } return false } func (p paginationQueryParams) getLinks(r *http.Request, count uint, info information) (result jsonapi.Links, err error) { result = make(jsonapi.Links) params := r.URL.Query() prefix := "" baseURL := info.GetBaseURL() if baseURL != "" { prefix = baseURL } requestURL := fmt.Sprintf("%s%s", prefix, r.URL.Path) if p.number != "" { // we have number & size params var number uint64 number, err = strconv.ParseUint(p.number, 10, 64) if err != nil { return } if p.number != "1" { params.Set("page[number]", "1") query, _ := url.QueryUnescape(params.Encode()) result["first"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)} params.Set("page[number]", strconv.FormatUint(number-1, 10)) query, _ = url.QueryUnescape(params.Encode()) result["prev"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)} } // calculate last page number var size uint64 size, err = strconv.ParseUint(p.size, 10, 64) if err != nil { return } totalPages := (uint64(count) / size) if (uint64(count) % size) != 0 { // there is one more page with some len(items) < size totalPages++ } if number != totalPages { params.Set("page[number]", strconv.FormatUint(number+1, 10)) query, _ := url.QueryUnescape(params.Encode()) result["next"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)} params.Set("page[number]", strconv.FormatUint(totalPages, 10)) query, _ = url.QueryUnescape(params.Encode()) result["last"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)} } } else { // we have offset & limit params var offset, limit uint64 offset, err = strconv.ParseUint(p.offset, 10, 64) if err != nil { return } limit, err = strconv.ParseUint(p.limit, 10, 64) if err != nil { return } if p.offset != "0" { params.Set("page[offset]", "0") query, _ := url.QueryUnescape(params.Encode()) result["first"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)} var prevOffset uint64 if limit > offset { prevOffset = 0 } else { prevOffset = offset - limit } params.Set("page[offset]", strconv.FormatUint(prevOffset, 10)) query, _ = url.QueryUnescape(params.Encode()) result["prev"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)} } // check if there are more entries to be loaded if (offset + limit) < uint64(count) { params.Set("page[offset]", strconv.FormatUint(offset+limit, 10)) query, _ := url.QueryUnescape(params.Encode()) result["next"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)} params.Set("page[offset]", strconv.FormatUint(uint64(count)-limit, 10)) query, _ = url.QueryUnescape(params.Encode()) result["last"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)} } } return } type notAllowedHandler struct { API *API } func (n notAllowedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { err := NewHTTPError(nil, "Method Not Allowed", http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed) contentType := defaultContentTypHeader if n.API != nil { contentType = n.API.ContentType } handleError(err, w, r, contentType) } type resource struct { resourceType reflect.Type source interface{} name string api *API } // middlewareChain executes the middleeware chain setup func (api *API) middlewareChain(c APIContexter, w http.ResponseWriter, r *http.Request) { for _, middleware := range api.middlewares { middleware(c, w, r) } } // allocateContext creates a context for the api.contextPool, saving allocations func (api *API) allocateDefaultContext() APIContexter { return &APIContext{} } func (api *API) addResource(prototype jsonapi.MarshalIdentifier, source interface{}) *resource { resourceType := reflect.TypeOf(prototype) if resourceType.Kind() != reflect.Struct && resourceType.Kind() != reflect.Ptr { panic("pass an empty resource struct or a struct pointer to AddResource!") } var ptrPrototype interface{} var name string if resourceType.Kind() == reflect.Struct { ptrPrototype = reflect.New(resourceType).Interface() name = resourceType.Name() } else { ptrPrototype = reflect.ValueOf(prototype).Interface() name = resourceType.Elem().Name() } // check if EntityNamer interface is implemented and use that as name entityName, ok := prototype.(jsonapi.EntityNamer) if ok { name = entityName.GetName() } else { name = jsonapi.Jsonify(jsonapi.Pluralize(name)) } res := resource{ resourceType: resourceType, name: name, source: source, api: api, } requestInfo := func(r *http.Request, api *API) *information { var info *information if resolver, ok := api.info.resolver.(RequestAwareURLResolver); ok { resolver.SetRequest(*r) info = &information{prefix: api.info.prefix, resolver: resolver} } else { info = &api.info } return info } prefix := strings.Trim(api.info.prefix, "/") baseURL := "/" + name if prefix != "" { baseURL = "/" + prefix + baseURL } api.router.Handle("OPTIONS", baseURL, func(w http.ResponseWriter, r *http.Request, _ map[string]string) { c := api.contextPool.Get().(APIContexter) c.Reset() api.middlewareChain(c, w, r) w.Header().Set("Allow", strings.Join(getAllowedMethods(source, true), ",")) w.WriteHeader(http.StatusNoContent) api.contextPool.Put(c) }) api.router.Handle("GET", baseURL, func(w http.ResponseWriter, r *http.Request, _ map[string]string) { info := requestInfo(r, api) c := api.contextPool.Get().(APIContexter) c.Reset() api.middlewareChain(c, w, r) err := res.handleIndex(c, w, r, *info) api.contextPool.Put(c) if err != nil { handleError(err, w, r, api.ContentType) } }) if _, ok := source.(ResourceGetter); ok { api.router.Handle("OPTIONS", baseURL+"/:id", func(w http.ResponseWriter, r *http.Request, _ map[string]string) { c := api.contextPool.Get().(APIContexter) c.Reset() api.middlewareChain(c, w, r) w.Header().Set("Allow", strings.Join(getAllowedMethods(source, false), ",")) w.WriteHeader(http.StatusNoContent) api.contextPool.Put(c) }) api.router.Handle("GET", baseURL+"/:id", func(w http.ResponseWriter, r *http.Request, params map[string]string) { info := requestInfo(r, api) c := api.contextPool.Get().(APIContexter) c.Reset() api.middlewareChain(c, w, r) err := res.handleRead(c, w, r, params, *info) api.contextPool.Put(c) if err != nil { handleError(err, w, r, api.ContentType) } }) } // generate all routes for linked relations if there are relations casted, ok := prototype.(jsonapi.MarshalReferences) if ok { relations := casted.GetReferences() for _, relation := range relations { api.router.Handle("GET", baseURL+"/:id/relationships/"+relation.Name, func(relation jsonapi.Reference) routing.HandlerFunc { return func(w http.ResponseWriter, r *http.Request, params map[string]string) { info := requestInfo(r, api) c := api.contextPool.Get().(APIContexter) c.Reset() api.middlewareChain(c, w, r) err := res.handleReadRelation(c, w, r, params, *info, relation) api.contextPool.Put(c) if err != nil { handleError(err, w, r, api.ContentType) } } }(relation)) api.router.Handle("GET", baseURL+"/:id/"+relation.Name, func(relation jsonapi.Reference) routing.HandlerFunc { return func(w http.ResponseWriter, r *http.Request, params map[string]string) { info := requestInfo(r, api) c := api.contextPool.Get().(APIContexter) c.Reset() api.middlewareChain(c, w, r) err := res.handleLinked(c, api, w, r, params, relation, *info) api.contextPool.Put(c) if err != nil { handleError(err, w, r, api.ContentType) } } }(relation)) api.router.Handle("PATCH", baseURL+"/:id/relationships/"+relation.Name, func(relation jsonapi.Reference) routing.HandlerFunc { return func(w http.ResponseWriter, r *http.Request, params map[string]string) { c := api.contextPool.Get().(APIContexter) c.Reset() api.middlewareChain(c, w, r) err := res.handleReplaceRelation(c, w, r, params, relation) api.contextPool.Put(c) if err != nil { handleError(err, w, r, api.ContentType) } } }(relation)) if _, ok := ptrPrototype.(jsonapi.EditToManyRelations); ok && relation.Name == jsonapi.Pluralize(relation.Name) { // generate additional routes to manipulate to-many relationships api.router.Handle("POST", baseURL+"/:id/relationships/"+relation.Name, func(relation jsonapi.Reference) routing.HandlerFunc { return func(w http.ResponseWriter, r *http.Request, params map[string]string) { c := api.contextPool.Get().(APIContexter) c.Reset() api.middlewareChain(c, w, r) err := res.handleAddToManyRelation(c, w, r, params, relation) api.contextPool.Put(c) if err != nil { handleError(err, w, r, api.ContentType) } } }(relation)) api.router.Handle("DELETE", baseURL+"/:id/relationships/"+relation.Name, func(relation jsonapi.Reference) routing.HandlerFunc { return func(w http.ResponseWriter, r *http.Request, params map[string]string) { c := api.contextPool.Get().(APIContexter) c.Reset() api.middlewareChain(c, w, r) err := res.handleDeleteToManyRelation(c, w, r, params, relation) api.contextPool.Put(c) if err != nil { handleError(err, w, r, api.ContentType) } } }(relation)) } } } if _, ok := source.(ResourceCreator); ok { api.router.Handle("POST", baseURL, func(w http.ResponseWriter, r *http.Request, params map[string]string) { info := requestInfo(r, api) c := api.contextPool.Get().(APIContexter) c.Reset() api.middlewareChain(c, w, r) err := res.handleCreate(c, w, r, info.prefix, *info) api.contextPool.Put(c) if err != nil { handleError(err, w, r, api.ContentType) } }) } if _, ok := source.(ResourceDeleter); ok { api.router.Handle("DELETE", baseURL+"/:id", func(w http.ResponseWriter, r *http.Request, params map[string]string) { c := api.contextPool.Get().(APIContexter) c.Reset() api.middlewareChain(c, w, r) err := res.handleDelete(c, w, r, params) api.contextPool.Put(c) if err != nil { handleError(err, w, r, api.ContentType) } }) } if _, ok := source.(ResourceUpdater); ok { api.router.Handle("PATCH", baseURL+"/:id", func(w http.ResponseWriter, r *http.Request, params map[string]string) { info := requestInfo(r, api) c := api.contextPool.Get().(APIContexter) c.Reset() api.middlewareChain(c, w, r) err := res.handleUpdate(c, w, r, params, *info) api.contextPool.Put(c) if err != nil { handleError(err, w, r, api.ContentType) } }) } api.resources = append(api.resources, res) return &res } func getAllowedMethods(source interface{}, collection bool) []string { result := []string{http.MethodOptions} if _, ok := source.(ResourceGetter); ok { result = append(result, http.MethodGet) } if _, ok := source.(ResourceUpdater); ok { result = append(result, http.MethodPatch) } if _, ok := source.(ResourceDeleter); ok && !collection { result = append(result, http.MethodDelete) } if _, ok := source.(ResourceCreator); ok && collection { result = append(result, http.MethodPost) } return result } func buildRequest(c APIContexter, r *http.Request) Request { req := Request{PlainRequest: r} params := make(map[string][]string) pagination := make(map[string]string) for key, values := range r.URL.Query() { params[key] = strings.Split(values[0], ",") pageMatches := queryPageRegex.FindStringSubmatch(key) if len(pageMatches) > 1 { pagination[pageMatches[1]] = values[0] } } req.Pagination = pagination req.QueryParams = params req.Header = r.Header req.Context = c return req } func (res *resource) marshalResponse(resp interface{}, w http.ResponseWriter, status int, r *http.Request) error { filtered, err := filterSparseFields(resp, r) if err != nil { return err } result, err := json.Marshal(filtered) if err != nil { return err } writeResult(w, result, status, res.api.ContentType) return nil } func (res *resource) handleIndex(c APIContexter, w http.ResponseWriter, r *http.Request, info information) error { if source, ok := res.source.(PaginatedFindAll); ok { pagination := newPaginationQueryParams(r) if pagination.isValid() { count, response, err := source.PaginatedFindAll(buildRequest(c, r)) if err != nil { return err } paginationLinks, err := pagination.getLinks(r, count, info) if err != nil { return err } return res.respondWithPagination(response, info, http.StatusOK, paginationLinks, w, r) } } source, ok := res.source.(FindAll) if !ok { return NewHTTPError(nil, "Resource does not implement the FindAll interface", http.StatusNotFound) } response, err := source.FindAll(buildRequest(c, r)) if err != nil { return err } return res.respondWith(response, info, http.StatusOK, w, r) } func (res *resource) handleRead(c APIContexter, w http.ResponseWriter, r *http.Request, params map[string]string, info information) error { source, ok := res.source.(ResourceGetter) if !ok { return fmt.Errorf("Resource %s does not implement the ResourceGetter interface", res.name) } id := params["id"] response, err := source.FindOne(id, buildRequest(c, r)) if err != nil { return err } return res.respondWith(response, info, http.StatusOK, w, r) } func (res *resource) handleReadRelation(c APIContexter, w http.ResponseWriter, r *http.Request, params map[string]string, info information, relation jsonapi.Reference) error { source, ok := res.source.(ResourceGetter) if !ok { return fmt.Errorf("Resource %s does not implement the ResourceGetter interface", res.name) } id := params["id"] obj, err := source.FindOne(id, buildRequest(c, r)) if err != nil { return err } document, err := jsonapi.MarshalToStruct(obj.Result(), info) if err != nil { return err } rel, ok := document.Data.DataObject.Relationships[relation.Name] if !ok { return NewHTTPError(nil, fmt.Sprintf("There is no relation with the name %s", relation.Name), http.StatusNotFound) } meta := obj.Metadata() if len(meta) > 0 { rel.Meta = meta } return res.marshalResponse(rel, w, http.StatusOK, r) } // try to find the referenced resource and call the findAll Method with referencing resource id as param func (res *resource) handleLinked(c APIContexter, api *API, w http.ResponseWriter, r *http.Request, params map[string]string, linked jsonapi.Reference, info information) error { id := params["id"] for _, resource := range api.resources { if resource.name == linked.Type { request := buildRequest(c, r) request.QueryParams[res.name+"ID"] = []string{id} request.QueryParams[res.name+"Name"] = []string{linked.Name} if source, ok := resource.source.(PaginatedFindAll); ok { // check for pagination, otherwise normal FindAll pagination := newPaginationQueryParams(r) if pagination.isValid() { var count uint count, response, err := source.PaginatedFindAll(request) if err != nil { return err } paginationLinks, err := pagination.getLinks(r, count, info) if err != nil { return err } return res.respondWithPagination(response, info, http.StatusOK, paginationLinks, w, r) } } source, ok := resource.source.(FindAll) if !ok { return NewHTTPError(nil, "Resource does not implement the FindAll interface", http.StatusNotFound) } obj, err := source.FindAll(request) if err != nil { return err } return res.respondWith(obj, info, http.StatusOK, w, r) } } return NewHTTPError( errors.New("Not Found"), "No resource handler is registered to handle the linked resource "+linked.Name, http.StatusNotFound, ) } func (res *resource) handleCreate(c APIContexter, w http.ResponseWriter, r *http.Request, prefix string, info information) error { source, ok := res.source.(ResourceCreator) if !ok { return fmt.Errorf("Resource %s does not implement the ResourceCreator interface", res.name) } ctx, err := unmarshalRequest(r) if err != nil { return err } // Ok this is weird again, but reflect.New produces a pointer, so we need the pure type without pointer, // otherwise we would have a pointer pointer type that we don't want. resourceType := res.resourceType if resourceType.Kind() == reflect.Ptr { resourceType = resourceType.Elem() } newObj := reflect.New(resourceType).Interface() // Call InitializeObject if available to allow implementers change the object // before calling Unmarshal. if initSource, ok := source.(ObjectInitializer); ok { initSource.InitializeObject(newObj) } err = jsonapi.Unmarshal(ctx, newObj) if err != nil { return NewHTTPError(nil, err.Error(), http.StatusNotAcceptable) } var response Responder if res.resourceType.Kind() == reflect.Struct { // we have to dereference the pointer if user wants to use non pointer values response, err = source.Create(reflect.ValueOf(newObj).Elem().Interface(), buildRequest(c, r)) } else { response, err = source.Create(newObj, buildRequest(c, r)) } if err != nil { return err } result, ok := response.Result().(jsonapi.MarshalIdentifier) if !ok { return fmt.Errorf("Expected one newly created object by resource %s", res.name) } if len(prefix) > 0 { w.Header().Set("Location", "/"+prefix+"/"+res.name+"/"+result.GetID()) } else { w.Header().Set("Location", "/"+res.name+"/"+result.GetID()) } // handle 200 status codes switch response.StatusCode() { case http.StatusCreated: return res.respondWith(response, info, http.StatusCreated, w, r) case http.StatusNoContent: w.WriteHeader(response.StatusCode()) return nil case http.StatusAccepted: w.WriteHeader(response.StatusCode()) return nil default: return fmt.Errorf("invalid status code %d from resource %s for method Create", response.StatusCode(), res.name) } } func (res *resource) handleUpdate(c APIContexter, w http.ResponseWriter, r *http.Request, params map[string]string, info information) error { source, ok := res.source.(ResourceUpdater) if !ok { return fmt.Errorf("Resource %s does not implement the ResourceUpdater interface", res.name) } id := params["id"] obj, err := source.FindOne(id, buildRequest(c, r)) if err != nil { return err } ctx, err := unmarshalRequest(r) if err != nil { return err } // we have to make the Result to a pointer to unmarshal into it updatingObj := reflect.ValueOf(obj.Result()) if updatingObj.Kind() == reflect.Struct { updatingObjPtr := reflect.New(reflect.TypeOf(obj.Result())) updatingObjPtr.Elem().Set(updatingObj) err = jsonapi.Unmarshal(ctx, updatingObjPtr.Interface()) updatingObj = updatingObjPtr.Elem() } else { err = jsonapi.Unmarshal(ctx, updatingObj.Interface()) } if err != nil { return NewHTTPError(nil, err.Error(), http.StatusNotAcceptable) } identifiable, ok := updatingObj.Interface().(jsonapi.MarshalIdentifier) if !ok || identifiable.GetID() != id { conflictError := errors.New("id in the resource does not match servers endpoint") return NewHTTPError(conflictError, conflictError.Error(), http.StatusConflict) } response, err := source.Update(updatingObj.Interface(), buildRequest(c, r)) if err != nil { return err } switch response.StatusCode() { case http.StatusOK: updated := response.Result() if updated == nil { internalResponse, err := source.FindOne(id, buildRequest(c, r)) if err != nil { return err } updated = internalResponse.Result() if updated == nil { return fmt.Errorf("Expected FindOne to return one object of resource %s", res.name) } response = internalResponse } return res.respondWith(response, info, http.StatusOK, w, r) case http.StatusAccepted: w.WriteHeader(http.StatusAccepted) return nil case http.StatusNoContent: w.WriteHeader(http.StatusNoContent) return nil default: return fmt.Errorf("invalid status code %d from resource %s for method Update", response.StatusCode(), res.name) } } func (res *resource) handleReplaceRelation(c APIContexter, w http.ResponseWriter, r *http.Request, params map[string]string, relation jsonapi.Reference) error { source, ok := res.source.(ResourceUpdater) if !ok { return fmt.Errorf("Resource %s does not implement the ResourceUpdater interface", res.name) } var ( err error editObj interface{} ) id := params["id"] response, err := source.FindOne(id, buildRequest(c, r)) if err != nil { return err } body, err := unmarshalRequest(r) if err != nil { return err } inc := map[string]interface{}{} err = json.Unmarshal(body, &inc) if err != nil { return err } data, ok := inc["data"] if !ok { return errors.New("Invalid object. Need a \"data\" object") } resType := reflect.TypeOf(response.Result()).Kind() if resType == reflect.Struct { editObj = getPointerToStruct(response.Result()) } else { editObj = response.Result() } err = processRelationshipsData(data, relation.Name, editObj) if err != nil { return err } if resType == reflect.Struct { _, err = source.Update(reflect.ValueOf(editObj).Elem().Interface(), buildRequest(c, r)) } else { _, err = source.Update(editObj, buildRequest(c, r)) } w.WriteHeader(http.StatusNoContent) return err } func (res *resource) handleAddToManyRelation(c APIContexter, w http.ResponseWriter, r *http.Request, params map[string]string, relation jsonapi.Reference) error { source, ok := res.source.(ResourceUpdater) if !ok { return fmt.Errorf("Resource %s does not implement the ResourceUpdater interface", res.name) } var ( err error editObj interface{} ) id := params["id"] response, err := source.FindOne(id, buildRequest(c, r)) if err != nil { return err } body, err := unmarshalRequest(r) if err != nil { return err } inc := map[string]interface{}{} err = json.Unmarshal(body, &inc) if err != nil { return err } data, ok := inc["data"] if !ok { return errors.New("Invalid object. Need a \"data\" object") } newRels, ok := data.([]interface{}) if !ok { return fmt.Errorf("Data must be an array with \"id\" and \"type\" field to add new to-many relationships") } newIDs := []string{} for _, newRel := range newRels { casted, ok := newRel.(map[string]interface{}) if !ok { return errors.New("entry in data object invalid") } newID, ok := casted["id"].(string) if !ok { return errors.New("no id field found inside data object") } newIDs = append(newIDs, newID) } resType := reflect.TypeOf(response.Result()).Kind() if resType == reflect.Struct { editObj = getPointerToStruct(response.Result()) } else { editObj = response.Result() } targetObj, ok := editObj.(jsonapi.EditToManyRelations) if !ok { return errors.New("target struct must implement jsonapi.EditToManyRelations") } targetObj.AddToManyIDs(relation.Name, newIDs) if resType == reflect.Struct { _, err = source.Update(reflect.ValueOf(targetObj).Elem().Interface(), buildRequest(c, r)) } else { _, err = source.Update(targetObj, buildRequest(c, r)) } w.WriteHeader(http.StatusNoContent) return err } func (res *resource) handleDeleteToManyRelation(c APIContexter, w http.ResponseWriter, r *http.Request, params map[string]string, relation jsonapi.Reference) error { source, ok := res.source.(ResourceUpdater) if !ok { return fmt.Errorf("Resource %s does not implement the ResourceUpdater interface", res.name) } var ( err error editObj interface{} ) id := params["id"] response, err := source.FindOne(id, buildRequest(c, r)) if err != nil { return err } body, err := unmarshalRequest(r) if err != nil { return err } inc := map[string]interface{}{} err = json.Unmarshal(body, &inc) if err != nil { return err } data, ok := inc["data"] if !ok { return errors.New("Invalid object. Need a \"data\" object") } newRels, ok := data.([]interface{}) if !ok { return fmt.Errorf("Data must be an array with \"id\" and \"type\" field to add new to-many relationships") } obsoleteIDs := []string{} for _, newRel := range newRels { casted, ok := newRel.(map[string]interface{}) if !ok { return errors.New("entry in data object invalid") } obsoleteID, ok := casted["id"].(string) if !ok { return errors.New("no id field found inside data object") } obsoleteIDs = append(obsoleteIDs, obsoleteID) } resType := reflect.TypeOf(response.Result()).Kind() if resType == reflect.Struct { editObj = getPointerToStruct(response.Result()) } else { editObj = response.Result() } targetObj, ok := editObj.(jsonapi.EditToManyRelations) if !ok { return errors.New("target struct must implement jsonapi.EditToManyRelations") } targetObj.DeleteToManyIDs(relation.Name, obsoleteIDs) if resType == reflect.Struct { _, err = source.Update(reflect.ValueOf(targetObj).Elem().Interface(), buildRequest(c, r)) } else { _, err = source.Update(targetObj, buildRequest(c, r)) } w.WriteHeader(http.StatusNoContent) return err } // returns a pointer to an interface{} struct func getPointerToStruct(oldObj interface{}) interface{} { resType := reflect.TypeOf(oldObj) ptr := reflect.New(resType) ptr.Elem().Set(reflect.ValueOf(oldObj)) return ptr.Interface() } func (res *resource) handleDelete(c APIContexter, w http.ResponseWriter, r *http.Request, params map[string]string) error { source, ok := res.source.(ResourceDeleter) if !ok { return fmt.Errorf("Resource %s does not implement the ResourceDeleter interface", res.name) } id := params["id"] response, err := source.Delete(id, buildRequest(c, r)) if err != nil { return err } switch response.StatusCode() { case http.StatusOK: data := map[string]interface{}{ "meta": response.Metadata(), } return res.marshalResponse(data, w, http.StatusOK, r) case http.StatusAccepted: w.WriteHeader(http.StatusAccepted) return nil case http.StatusNoContent: w.WriteHeader(http.StatusNoContent) return nil default: return fmt.Errorf("invalid status code %d from resource %s for method Delete", response.StatusCode(), res.name) } } func writeResult(w http.ResponseWriter, data []byte, status int, contentType string) { w.Header().Set("Content-Type", contentType) w.WriteHeader(status) w.Write(data) } func (res *resource) respondWith(obj Responder, info information, status int, w http.ResponseWriter, r *http.Request) error { data, err := jsonapi.MarshalToStruct(obj.Result(), info) if err != nil { return err } meta := obj.Metadata() if len(meta) > 0 { data.Meta = meta } if objWithLinks, ok := obj.(LinksResponder); ok { baseURL := strings.Trim(info.GetBaseURL(), "/") requestURL := fmt.Sprintf("%s%s", baseURL, r.URL.Path) links := objWithLinks.Links(r, requestURL) if len(links) > 0 { data.Links = links } } return res.marshalResponse(data, w, status, r) } func (res *resource) respondWithPagination(obj Responder, info information, status int, links jsonapi.Links, w http.ResponseWriter, r *http.Request) error { data, err := jsonapi.MarshalToStruct(obj.Result(), info) if err != nil { return err } data.Links = links meta := obj.Metadata() if len(meta) > 0 { data.Meta = meta } return res.marshalResponse(data, w, status, r) } func unmarshalRequest(r *http.Request) ([]byte, error) { defer r.Body.Close() data, err := ioutil.ReadAll(r.Body) if err != nil { return nil, err } return data, nil } func filterSparseFields(resp interface{}, r *http.Request) (interface{}, error) { query := r.URL.Query() queryParams := parseQueryFields(&query) if len(queryParams) < 1 { return resp, nil } if document, ok := resp.(*jsonapi.Document); ok { wrongFields := map[string][]string{} // single entry in data data := document.Data.DataObject if data != nil { errors := replaceAttributes(&queryParams, data) for t, v := range errors { wrongFields[t] = v } } // data can be a slice too datas := document.Data.DataArray for index, data := range datas { errors := replaceAttributes(&queryParams, &data) for t, v := range errors { wrongFields[t] = v } datas[index] = data } // included slice for index, include := range document.Included { errors := replaceAttributes(&queryParams, &include) for t, v := range errors { wrongFields[t] = v } document.Included[index] = include } if len(wrongFields) > 0 { httpError := NewHTTPError(nil, "Some requested fields were invalid", http.StatusBadRequest) for k, v := range wrongFields { for _, field := range v { httpError.Errors = append(httpError.Errors, Error{ Status: "Bad Request", Code: codeInvalidQueryFields, Title: fmt.Sprintf(`Field "%s" does not exist for type "%s"`, field, k), Detail: "Please make sure you do only request existing fields", Source: &ErrorSource{ Parameter: fmt.Sprintf("fields[%s]", k), }, }) } } return nil, httpError } } return resp, nil } func parseQueryFields(query *url.Values) (result map[string][]string) { result = map[string][]string{} for name, param := range *query { matches := queryFieldsRegex.FindStringSubmatch(name) if len(matches) > 1 { match := matches[1] result[match] = strings.Split(param[0], ",") } } return } func filterAttributes(attributes map[string]interface{}, fields []string) (filteredAttributes map[string]interface{}, wrongFields []string) { wrongFields = []string{} filteredAttributes = map[string]interface{}{} for _, field := range fields { if attribute, ok := attributes[field]; ok { filteredAttributes[field] = attribute } else { wrongFields = append(wrongFields, field) } } return } func replaceAttributes(query *map[string][]string, entry *jsonapi.Data) map[string][]string { fieldType := entry.Type attributes := map[string]interface{}{} _ = json.Unmarshal(entry.Attributes, &attributes) fields := (*query)[fieldType] if len(fields) > 0 { var wrongFields []string attributes, wrongFields = filterAttributes(attributes, fields) if len(wrongFields) > 0 { return map[string][]string{ fieldType: wrongFields, } } bytes, _ := json.Marshal(attributes) entry.Attributes = bytes } return nil } func handleError(err error, w http.ResponseWriter, r *http.Request, contentType string) { log.Println(err) if e, ok := err.(HTTPError); ok { writeResult(w, []byte(marshalHTTPError(e)), e.status, contentType) return } e := NewHTTPError(err, err.Error(), http.StatusInternalServerError) writeResult(w, []byte(marshalHTTPError(e)), http.StatusInternalServerError, contentType) } // TODO: this can also be replaced with a struct into that we directly json.Unmarshal func processRelationshipsData(data interface{}, linkName string, target interface{}) error { hasOne, ok := data.(map[string]interface{}) if ok { hasOneID, ok := hasOne["id"].(string) if !ok { return fmt.Errorf("data object must have a field id for %s", linkName) } target, ok := target.(jsonapi.UnmarshalToOneRelations) if !ok { return errors.New("target struct must implement interface UnmarshalToOneRelations") } target.SetToOneReferenceID(linkName, hasOneID) } else if data == nil { // this means that a to-one relationship must be deleted target, ok := target.(jsonapi.UnmarshalToOneRelations) if !ok { return errors.New("target struct must implement interface UnmarshalToOneRelations") } target.SetToOneReferenceID(linkName, "") } else { hasMany, ok := data.([]interface{}) if !ok { return fmt.Errorf("invalid data object or array, must be an object with \"id\" and \"type\" field for %s", linkName) } target, ok := target.(jsonapi.UnmarshalToManyRelations) if !ok { return errors.New("target struct must implement interface UnmarshalToManyRelations") } hasManyIDs := []string{} for _, entry := range hasMany { data, ok := entry.(map[string]interface{}) if !ok { return fmt.Errorf("entry in data array must be an object for %s", linkName) } dataID, ok := data["id"].(string) if !ok { return fmt.Errorf("all data objects must have a field id for %s", linkName) } hasManyIDs = append(hasManyIDs, dataID) } target.SetToManyReferenceIDs(linkName, hasManyIDs) } return nil } api2go-1.0-RC4/api2go_suite_test.go000066400000000000000000000003551317567713700171250ustar00rootroot00000000000000package api2go import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "io/ioutil" "log" "testing" ) func TestApi2go(t *testing.T) { RegisterFailHandler(Fail) log.SetOutput(ioutil.Discard) RunSpecs(t, "Api2go Suite") } api2go-1.0-RC4/api_entity_name_test.go000066400000000000000000000067201317567713700177020ustar00rootroot00000000000000package api2go import ( "net/http" "net/http/httptest" "strings" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) type BaguetteTaste struct { ID string `json:"-"` Taste string `json:"taste"` } func (s BaguetteTaste) GetID() string { return s.ID } func (s *BaguetteTaste) SetID(ID string) error { s.ID = ID return nil } func (s BaguetteTaste) GetName() string { return "baguette-tastes" } type BaguetteResource struct{} func (s BaguetteResource) FindOne(ID string, req Request) (Responder, error) { return &Response{Res: BaguetteTaste{ID: "blubb", Taste: "Very Bad"}}, nil } func (s BaguetteResource) FindAll(req Request) (Responder, error) { return &Response{Res: []BaguetteTaste{ { ID: "1", Taste: "Very Good", }, { ID: "2", Taste: "Very Bad", }, }}, nil } func (s BaguetteResource) Create(obj interface{}, req Request) (Responder, error) { e := obj.(BaguetteTaste) e.ID = "newID" return &Response{ Res: e, Code: http.StatusCreated, }, nil } func (s BaguetteResource) Delete(ID string, req Request) (Responder, error) { return &Response{ Res: BaguetteTaste{ID: ID}, Code: http.StatusNoContent, }, nil } func (s BaguetteResource) Update(obj interface{}, req Request) (Responder, error) { return &Response{ Res: obj, Code: http.StatusNoContent, }, nil } var _ = Describe("Test route renaming with EntityNamer interface", func() { var ( api *API rec *httptest.ResponseRecorder body *strings.Reader ) BeforeEach(func() { api = NewAPIWithRouting(testPrefix, NewStaticResolver(""), newTestRouter()) api.AddResource(BaguetteTaste{}, BaguetteResource{}) rec = httptest.NewRecorder() body = strings.NewReader(` { "data": { "attributes": { "taste": "smells awful" }, "id": "blubb", "type": "baguette-tastes" } } `) }) // check that renaming works, we do not test every single route here, the name variable is used // for each route, we just check the 5 basic ones. Marshalling and Unmarshalling is tested with // this again too. It("FindAll returns 200", func() { req, err := http.NewRequest("GET", "/v1/baguette-tastes", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) }) It("FindOne", func() { req, err := http.NewRequest("GET", "/v1/baguette-tastes/12345", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) }) It("Delete", func() { req, err := http.NewRequest("DELETE", "/v1/baguette-tastes/12345", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNoContent)) }) It("Create", func() { req, err := http.NewRequest("POST", "/v1/baguette-tastes", body) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) // the response is always the one record returned by FindOne, the implementation does not // check the ID here and returns something new ... Expect(rec.Body.String()).To(MatchJSON(` { "data": { "attributes": { "taste": "smells awful" }, "id": "newID", "type": "baguette-tastes" } } `)) Expect(rec.Code).To(Equal(http.StatusCreated)) }) It("Update", func() { req, err := http.NewRequest("PATCH", "/v1/baguette-tastes/blubb", body) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Body.String()).To(Equal("")) Expect(rec.Code).To(Equal(http.StatusNoContent)) }) }) api2go-1.0-RC4/api_interfaces.go000066400000000000000000000111531317567713700164460ustar00rootroot00000000000000package api2go import ( "net/http" "github.com/manyminds/api2go/jsonapi" ) // The ResourceGetter interface MUST be implemented in order to generate the single GET route and related type ResourceGetter interface { // FindOne returns an object by its ID // Possible Responder success status code 200 FindOne(ID string, req Request) (Responder, error) } // The CRUD interface embed all interfaces at once: `ResourceCreator`, `ResourceDeleter`, `ResourceUpdater` (which includes `ResourceGetter`) type CRUD interface { ResourceCreator ResourceDeleter ResourceUpdater } // The ResourceCreator interface MUST be implemented in order to generate the POST route type ResourceCreator interface { // Create a new object. Newly created object/struct must be in Responder. // Possible Responder status codes are: // - 201 Created: Resource was created and needs to be returned // - 202 Accepted: Processing is delayed, return nothing // - 204 No Content: Resource created with a client generated ID, and no fields were modified by // the server Create(obj interface{}, req Request) (Responder, error) } // The ResourceDeleter interface MUST be implemented in order to generate the DELETE route type ResourceDeleter interface { // Delete an object // Possible Responder status codes are: // - 200 OK: Deletion was a success, returns meta information, currently not implemented! Do not use this // - 202 Accepted: Processing is delayed, return nothing // - 204 No Content: Deletion was successful, return nothing Delete(id string, req Request) (Responder, error) } // The ResourceUpdater interface MUST be implemented in order to generate the PATCH/PUT routes type ResourceUpdater interface { // ResourceGetter must be implemented along with ResourceUpdater so that api2go can retrieve the single resource before update ResourceGetter // Update an object // Possible Responder status codes are: // - 200 OK: Update successful, however some field(s) were changed, returns updates source // - 202 Accepted: Processing is delayed, return nothing // - 204 No Content: Update was successful, no fields were changed by the server, return nothing Update(obj interface{}, req Request) (Responder, error) } // Pagination represents information needed to return pagination links type Pagination struct { Next map[string]string Prev map[string]string First map[string]string Last map[string]string } // The PaginatedFindAll interface can be optionally implemented to fetch a subset of all records. // Pagination query parameters must be used to limit the result. Pagination URLs will automatically // be generated by the api. You can use a combination of the following 2 query parameters: // page[number] AND page[size] // OR page[offset] AND page[limit] type PaginatedFindAll interface { PaginatedFindAll(req Request) (totalCount uint, response Responder, err error) } // The FindAll interface can be optionally implemented to fetch all records at once. type FindAll interface { // FindAll returns all objects FindAll(req Request) (Responder, error) } // The ObjectInitializer interface can be implemented to have the ability to change // a created object before Unmarshal is called. This is currently only called on // Create as the other actions go through FindOne or FindAll which are already // controlled by the implementer. type ObjectInitializer interface { InitializeObject(interface{}) } //URLResolver allows you to implement a static //way to return a baseURL for all incoming //requests for one api2go instance. type URLResolver interface { GetBaseURL() string } // RequestAwareURLResolver allows you to dynamically change // generated urls. // // This is particulary useful if you have the same // API answering to multiple domains, or subdomains // e.g customer[1,2,3,4].yourapi.example.com // // SetRequest will always be called prior to // the GetBaseURL() from `URLResolver` so you // have to change the result value based on the last // request. type RequestAwareURLResolver interface { URLResolver SetRequest(http.Request) } // The Responder interface is used by all Resource Methods as a container for the Response. // Metadata is additional Metadata. You can put anything you like into it, see jsonapi spec. // Result returns the actual payload. For FindOne, put only one entry in it. // StatusCode sets the http status code. type Responder interface { Metadata() map[string]interface{} Result() interface{} StatusCode() int } // The LinksResponder interface may be used when the response object is able to return // a set of links for the top-level response object. type LinksResponder interface { Responder Links(*http.Request, string) jsonapi.Links } api2go-1.0-RC4/api_interfaces_test.go000066400000000000000000000347221317567713700175140ustar00rootroot00000000000000package api2go import ( "encoding/json" "net/http" "net/http/httptest" "strconv" "strings" "github.com/manyminds/api2go/jsonapi" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) type SomeData struct { ID string `json:"-"` Data string `json:"data"` CustomerID string `json:"customerId"` } func (s SomeData) GetID() string { return s.ID } func (s *SomeData) SetID(ID string) error { s.ID = ID return nil } type SomeResource struct{} func (s SomeResource) FindOne(ID string, req Request) (Responder, error) { return &Response{Res: SomeData{ID: "12345", Data: "A Brezzn"}}, nil } func (s SomeResource) Create(obj interface{}, req Request) (Responder, error) { incoming := obj.(SomeData) switch incoming.ID { case "": return &Response{Res: SomeData{ID: "12345", Data: "A Brezzn"}, Code: http.StatusCreated}, nil case "accept": return &Response{Res: SomeData{ID: "someID"}, Code: http.StatusAccepted}, nil case "forbidden": return &Response{}, NewHTTPError(nil, "Forbidden", http.StatusForbidden) case "conflict": return &Response{}, NewHTTPError(nil, "Conflict", http.StatusConflict) case "invalid": return &Response{Res: SomeData{}, Code: http.StatusTeapot}, nil default: return &Response{Res: SomeData{ID: incoming.ID}, Code: http.StatusNoContent}, nil } } func (s SomeResource) Delete(ID string, req Request) (Responder, error) { switch ID { case "200": return &Response{Code: http.StatusOK, Meta: map[string]interface{}{"some": "cool stuff"}}, nil case "202": return &Response{Code: http.StatusAccepted}, nil default: return &Response{Code: http.StatusNoContent}, nil } } func (s SomeResource) Update(obj interface{}, req Request) (Responder, error) { incoming := obj.(SomeData) switch incoming.Data { case "override me": return &Response{Code: http.StatusOK}, nil case "delayed": return &Response{Code: http.StatusAccepted}, nil case "new value": return &Response{Code: http.StatusNoContent}, nil case "fail": return &Response{}, NewHTTPError(nil, "Fail", http.StatusForbidden) case "invalid": return &Response{Code: http.StatusTeapot}, nil default: return &Response{Code: http.StatusNoContent}, nil } } type ResourceReadOnly struct{} func (r ResourceReadOnly) FindOne(ID string, req Request) (Responder, error) { return &Response{Res: SomeData{ID: "12345", Data: "A Brezzn"}, Code: http.StatusOK}, nil } type ResourceCreationOnly struct{} func (r ResourceCreationOnly) Create(obj interface{}, req Request) (Responder, error) { return &Response{Res: SomeData{ID: "12345", Data: "A Brezzn"}, Code: http.StatusCreated}, nil } type ResourceUpdateOnly struct{} func (r ResourceUpdateOnly) FindOne(ID string, req Request) (Responder, error) { return &Response{Res: SomeData{ID: "12345", Data: "A Brezzn"}, Code: http.StatusOK}, nil } func (r ResourceUpdateOnly) Update(obj interface{}, req Request) (Responder, error) { return &Response{Res: SomeData{ID: "12345", Data: "A Brezzn"}, Code: http.StatusOK}, nil } type ResourceDeletionOnly struct{} func (r ResourceDeletionOnly) Delete(id string, req Request) (Responder, error) { return &Response{Code: http.StatusNoContent}, nil } var _ = Describe("Test interface api type casting", func() { var ( api *API rec *httptest.ResponseRecorder ) BeforeEach(func() { api = NewAPI("v1") api.AddResource(SomeData{}, SomeResource{}) rec = httptest.NewRecorder() }) It("FindAll returns 404 for simple CRUD", func() { req, err := http.NewRequest("GET", "/v1/someDatas", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNotFound)) }) It("Works for a normal FindOne", func() { req, err := http.NewRequest("GET", "/v1/someDatas/12345", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) }) It("Post works with lowercase renaming", func() { reqBody := strings.NewReader(`{"data": {"attributes":{"customerId": "2" }, "type": "someDatas"}}`) req, err := http.NewRequest("POST", "/v1/someDatas", reqBody) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusCreated)) }) }) var _ = Describe("Test return code behavior", func() { var ( api *API rec *httptest.ResponseRecorder payload, payloadID SomeData ) BeforeEach(func() { api = NewAPI("v1") api.AddResource(SomeData{}, SomeResource{}) rec = httptest.NewRecorder() payloadID = SomeData{ID: "12345", Data: "A Brezzn"} payload = SomeData{Data: "A Brezzn"} }) Context("Create", func() { post := func(payload SomeData) { m, err := jsonapi.Marshal(payload) Expect(err).ToNot(HaveOccurred()) req, err := http.NewRequest("POST", "/v1/someDatas", strings.NewReader(string(m))) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) } It("returns object with 201 created", func() { post(payload) Expect(rec.Code).To(Equal(http.StatusCreated)) var actual SomeData err := jsonapi.Unmarshal(rec.Body.Bytes(), &actual) Expect(err).ToNot(HaveOccurred()) Expect(payloadID).To(Equal(actual)) }) It("return no content 204 with client side generated id", func() { post(payloadID) Expect(rec.Code).To(Equal(http.StatusNoContent)) Expect(rec.Body.String()).To(BeEmpty()) }) It("return accepted and no content", func() { post(SomeData{ID: "accept", Data: "nothing"}) Expect(rec.Code).To(Equal(http.StatusAccepted)) Expect(rec.Body.String()).To(BeEmpty()) }) It("does not accept invalid return codes", func() { post(SomeData{ID: "invalid"}) Expect(rec.Code).To(Equal(http.StatusInternalServerError)) var err HTTPError json.Unmarshal(rec.Body.Bytes(), &err) Expect(err.Errors[0]).To(Equal(Error{ Title: "invalid status code 418 from resource someDatas for method Create", Status: strconv.Itoa(http.StatusInternalServerError)})) }) It("handles forbidden 403 error", func() { post(SomeData{ID: "forbidden", Data: "i am so forbidden"}) Expect(rec.Code).To(Equal(http.StatusForbidden)) var err HTTPError json.Unmarshal(rec.Body.Bytes(), &err) Expect(err.Errors[0]).To(Equal(Error{Title: "Forbidden", Status: strconv.Itoa(http.StatusForbidden)})) }) It("handles 409 conflict error", func() { post(SomeData{ID: "conflict", Data: "no force push here"}) Expect(rec.Code).To(Equal(http.StatusConflict)) var err HTTPError json.Unmarshal(rec.Body.Bytes(), &err) Expect(err.Errors[0]).To(Equal(Error{Title: "Conflict", Status: strconv.Itoa(http.StatusConflict)})) }) }) Context("Update", func() { patch := func(payload SomeData) { m, err := jsonapi.Marshal(payload) Expect(err).ToNot(HaveOccurred()) req, err := http.NewRequest("PATCH", "/v1/someDatas/12345", strings.NewReader(string(m))) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) } It("returns 200 ok if the server modified a field", func() { patch(SomeData{ID: "12345", Data: "override me"}) Expect(rec.Code).To(Equal(http.StatusOK)) var actual SomeData err := jsonapi.Unmarshal(rec.Body.Bytes(), &actual) Expect(err).ToNot(HaveOccurred()) Expect(payloadID).To(Equal(actual)) }) It("returns 202 Accepted if update is delayed", func() { patch(SomeData{ID: "12345", Data: "delayed"}) Expect(rec.Code).To(Equal(http.StatusAccepted)) Expect(rec.Body.String()).To(BeEmpty()) }) It("returns 204 No Content if update was accepted", func() { patch(SomeData{ID: "12345", Data: "new value"}) Expect(rec.Code).To(Equal(http.StatusNoContent)) Expect(rec.Body.String()).To(BeEmpty()) }) It("does not accept invalid return codes", func() { patch(SomeData{ID: "12345", Data: "invalid"}) Expect(rec.Code).To(Equal(http.StatusInternalServerError)) var err HTTPError json.Unmarshal(rec.Body.Bytes(), &err) Expect(err.Errors[0]).To(Equal(Error{ Title: "invalid status code 418 from resource someDatas for method Update", Status: strconv.Itoa(http.StatusInternalServerError)})) }) // We do not check everything again like in Create, because it's always the same handleError // method that get's called. It("handles error cases", func() { patch(SomeData{ID: "12345", Data: "fail"}) Expect(rec.Code).To(Equal(http.StatusForbidden), "we do not allow failes here!") var err HTTPError json.Unmarshal(rec.Body.Bytes(), &err) Expect(err.Errors[0]).To(Equal(Error{Title: "Fail", Status: strconv.Itoa(http.StatusForbidden)})) }) }) Context("Delete", func() { delete := func(ID string) { req, err := http.NewRequest("DELETE", "/v1/someDatas/"+ID, nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) } It("returns 200 ok if there is some meta data", func() { delete("200") Expect(rec.Code).To(Equal(http.StatusOK)) var response map[string]interface{} json.Unmarshal(rec.Body.Bytes(), &response) Expect(response).To(Equal(map[string]interface{}{ "meta": map[string]interface{}{ "some": "cool stuff", }, })) }) It("returns 202 accepted if deletion is delayed", func() { delete("202") Expect(rec.Code).To(Equal(http.StatusAccepted)) Expect(rec.Body.String()).To(BeEmpty()) }) It("return 204 No Content if deletion just worked", func() { delete("204") Expect(rec.Code).To(Equal(http.StatusNoContent)) Expect(rec.Body.String()).To(BeEmpty()) }) }) }) var _ = Describe("Test partial CRUD implementation : Creator", func() { var ( api *API rec *httptest.ResponseRecorder payload, payloadID SomeData ) BeforeEach(func() { api = NewAPI("v1") api.AddResource(SomeData{}, ResourceCreationOnly{}) rec = httptest.NewRecorder() payloadID = SomeData{ID: "12345", Data: "A Brezzn"} payload = SomeData{Data: "A Brezzn"} }) Context("Create", func() { post := func(payload SomeData) { m, err := jsonapi.Marshal(payload) Expect(err).ToNot(HaveOccurred()) req, err := http.NewRequest("POST", "/v1/someDatas", strings.NewReader(string(m))) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) } It("returns 201", func() { post(payload) Expect(rec.Code).To(Equal(http.StatusCreated)) var actual SomeData err := jsonapi.Unmarshal(rec.Body.Bytes(), &actual) Expect(err).ToNot(HaveOccurred()) Expect(payloadID).To(Equal(actual)) }) It("returns 404 on every other routes", func() { // Test PATCH m, err := jsonapi.Marshal(payload) Expect(err).ToNot(HaveOccurred()) reqPatch, errPatch := http.NewRequest("PATCH", "/v1/someDatas/12345", strings.NewReader(string(m))) Expect(errPatch).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, reqPatch) Expect(rec.Code).To(Equal(http.StatusNotFound)) // Test DELETE reqDelete, errDelete := http.NewRequest("DELETE", "/v1/someDatas/12345", nil) Expect(errDelete).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, reqDelete) Expect(rec.Code).To(Equal(http.StatusNotFound)) // Test GET item reqRead, errRead := http.NewRequest("GET", "/v1/someDatas/12345", nil) Expect(errRead).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, reqRead) Expect(rec.Code).To(Equal(http.StatusNotFound)) }) }) }) var _ = Describe("Test partial CRUD implementation : Updater", func() { var ( api *API rec *httptest.ResponseRecorder payload, payloadID SomeData ) BeforeEach(func() { api = NewAPI("v1") api.AddResource(SomeData{}, ResourceUpdateOnly{}) rec = httptest.NewRecorder() payloadID = SomeData{ID: "12345", Data: "A Brezzn"} payload = SomeData{Data: "A Brezzn"} }) Context("Update", func() { patch := func(payload SomeData) { m, err := jsonapi.Marshal(payload) Expect(err).ToNot(HaveOccurred()) req, err := http.NewRequest("PATCH", "/v1/someDatas/12345", strings.NewReader(string(m))) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) } It("returns 200", func() { patch(SomeData{ID: "12345", Data: "override me"}) Expect(rec.Code).To(Equal(http.StatusOK)) var actual SomeData err := jsonapi.Unmarshal(rec.Body.Bytes(), &actual) Expect(err).ToNot(HaveOccurred()) Expect(payloadID).To(Equal(actual)) }) It("returns 404 on every other routes", func() { // Test POST m, err := jsonapi.Marshal(payload) Expect(err).ToNot(HaveOccurred()) reqPost, errPost := http.NewRequest("POST", "/v1/someDatas", strings.NewReader(string(m))) Expect(errPost).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, reqPost) Expect(rec.Code).To(Equal(http.StatusMethodNotAllowed)) // Test DELETE reqDelete, errDelete := http.NewRequest("DELETE", "/v1/someDatas/12345", nil) Expect(errDelete).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, reqDelete) Expect(rec.Code).To(Equal(http.StatusMethodNotAllowed)) // Test GET item reqRead, errRead := http.NewRequest("GET", "/v1/someDatas/12345", nil) Expect(errRead).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, reqRead) Expect(rec.Code).To(Equal(http.StatusMethodNotAllowed)) }) }) }) var _ = Describe("Test partial CRUD implementation : Deleter", func() { var ( api *API rec *httptest.ResponseRecorder payload, payloadID SomeData ) BeforeEach(func() { api = NewAPI("v1") api.AddResource(SomeData{}, ResourceDeletionOnly{}) rec = httptest.NewRecorder() payloadID = SomeData{ID: "12345", Data: "A Brezzn"} payload = SomeData{Data: "A Brezzn"} }) Context("Delete", func() { delete := func() { req, err := http.NewRequest("DELETE", "/v1/someDatas/1234", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) } It("returns 204", func() { delete() Expect(rec.Code).To(Equal(http.StatusNoContent)) }) It("returns 404 on every other routes", func() { // Test POST m, err := jsonapi.Marshal(payload) Expect(err).ToNot(HaveOccurred()) reqPost, errPost := http.NewRequest("POST", "/v1/someDatas", strings.NewReader(string(m))) Expect(errPost).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, reqPost) Expect(rec.Code).To(Equal(http.StatusMethodNotAllowed)) // Test PATCH reqPatch, errPatch := http.NewRequest("PATCH", "/v1/someDatas/12345", strings.NewReader(string(m))) Expect(errPatch).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, reqPatch) Expect(rec.Code).To(Equal(http.StatusMethodNotAllowed)) // Test GET item reqRead, errRead := http.NewRequest("GET", "/v1/someDatas/12345", nil) Expect(errRead).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, reqRead) Expect(rec.Code).To(Equal(http.StatusMethodNotAllowed)) }) }) }) api2go-1.0-RC4/api_object_initializer_test.go000066400000000000000000000043441317567713700212370ustar00rootroot00000000000000package api2go import ( "net/http" "net/http/httptest" "strings" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) type ObjectInitializerResource struct{} func (s ObjectInitializerResource) InitializeObject(obj interface{}) { if post, ok := obj.(*Post); ok { post.Title = "New Title" } } func (s ObjectInitializerResource) FindOne(ID string, req Request) (Responder, error) { return nil, nil } func (s ObjectInitializerResource) Create(obj interface{}, req Request) (Responder, error) { return &Response{Res: obj, Code: http.StatusCreated}, nil } func (s ObjectInitializerResource) Delete(ID string, req Request) (Responder, error) { return nil, nil } func (s ObjectInitializerResource) Update(obj interface{}, req Request) (Responder, error) { return nil, nil } var _ = Describe("Test resource implementing the ObjectInitializer interface", func() { var ( api *API rec *httptest.ResponseRecorder body *strings.Reader ) BeforeEach(func() { api = NewAPIWithRouting(testPrefix, NewStaticResolver(""), newTestRouter()) api.AddResource(Post{}, ObjectInitializerResource{}) rec = httptest.NewRecorder() body = strings.NewReader(` { "data": { "attributes": {}, "id": "blubb", "type": "posts" } } `) }) It("Create", func() { req, err := http.NewRequest("POST", "/v1/posts", body) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Body.String()).To(MatchJSON(` { "data": { "type": "posts", "id": "blubb", "attributes": { "title": "New Title", "value": null }, "relationships": { "author": { "links": { "self": "/v1/posts/blubb/relationships/author", "related": "/v1/posts/blubb/author" }, "data": null }, "bananas": { "links": { "self": "/v1/posts/blubb/relationships/bananas", "related": "/v1/posts/blubb/bananas" }, "data": [] }, "comments": { "links": { "self": "/v1/posts/blubb/relationships/comments", "related": "/v1/posts/blubb/comments" }, "data": [] } } } } `)) Expect(rec.Code).To(Equal(http.StatusCreated)) }) }) api2go-1.0-RC4/api_public.go000066400000000000000000000105721317567713700156050ustar00rootroot00000000000000package api2go import ( "net/http" "strings" "sync" "github.com/manyminds/api2go/jsonapi" "github.com/manyminds/api2go/routing" ) // HandlerFunc for api2go middlewares type HandlerFunc func(APIContexter, http.ResponseWriter, *http.Request) // API is a REST JSONAPI. type API struct { ContentType string router routing.Routeable info information resources []resource middlewares []HandlerFunc contextPool sync.Pool contextAllocator APIContextAllocatorFunc } // Handler returns the http.Handler instance for the API. func (api API) Handler() http.Handler { return api.router.Handler() } //Router returns the specified router on an api instance func (api API) Router() routing.Routeable { return api.router } // SetContextAllocator custom implementation for making contexts func (api *API) SetContextAllocator(allocator APIContextAllocatorFunc) { api.contextAllocator = allocator } // AddResource registers a data source for the given resource // At least the CRUD interface must be implemented, all the other interfaces are optional. // `resource` should be either an empty struct instance such as `Post{}` or a pointer to // a struct such as `&Post{}`. The same type will be used for constructing new elements. func (api *API) AddResource(prototype jsonapi.MarshalIdentifier, source interface{}) { api.addResource(prototype, source) } // UseMiddleware registers middlewares that implement the api2go.HandlerFunc // Middleware is run before any generated routes. func (api *API) UseMiddleware(middleware ...HandlerFunc) { api.middlewares = append(api.middlewares, middleware...) } // NewAPIVersion can be used to chain an additional API version to the routing of a previous // one. Use this if you have multiple version prefixes and want to combine all // your different API versions. This reuses the baseURL or URLResolver func (api *API) NewAPIVersion(prefix string) *API { return newAPI(prefix, api.info.resolver, api.router) } // NewAPIWithResolver can be used to create an API with a custom URL resolver. func NewAPIWithResolver(prefix string, resolver URLResolver) *API { handler := notAllowedHandler{} r := routing.NewHTTPRouter(prefix, &handler) api := newAPI(prefix, resolver, r) handler.API = api return api } // NewAPIWithBaseURL does the same as NewAPI with the addition of // a baseURL which get's added in front of all generated URLs. // For example http://localhost/v1/myResource/abc instead of /v1/myResource/abc func NewAPIWithBaseURL(prefix string, baseURL string) *API { handler := notAllowedHandler{} staticResolver := NewStaticResolver(baseURL) r := routing.NewHTTPRouter(prefix, &handler) api := newAPI(prefix, staticResolver, r) handler.API = api return api } // NewAPI returns an initialized API instance // `prefix` is added in front of all endpoints. func NewAPI(prefix string) *API { handler := notAllowedHandler{} staticResolver := NewStaticResolver("") r := routing.NewHTTPRouter(prefix, &handler) api := newAPI(prefix, staticResolver, r) handler.API = api return api } // NewAPIWithRouting allows you to use a custom URLResolver, marshalers and custom routing // if you want to use the default routing, you should use another constructor. // // If you don't need any of the parameters you can skip them with the defaults: // the default for `prefix` would be `""`, which means there is no namespace for your api. // although we suggest using one. // // if your api only answers to one url you can use a NewStaticResolver() as `resolver` func NewAPIWithRouting(prefix string, resolver URLResolver, router routing.Routeable) *API { return newAPI(prefix, resolver, router) } // newAPI is now an internal method that can be changed if params are changing func newAPI(prefix string, resolver URLResolver, router routing.Routeable) *API { // Add initial and trailing slash to prefix prefixSlashes := strings.Trim(prefix, "/") if len(prefixSlashes) > 0 { prefixSlashes = "/" + prefixSlashes + "/" } else { prefixSlashes = "/" } info := information{prefix: prefix, resolver: resolver} api := &API{ ContentType: defaultContentTypHeader, router: router, info: info, middlewares: make([]HandlerFunc, 0), contextAllocator: nil, } api.contextPool.New = func() interface{} { if api.contextAllocator != nil { return api.contextAllocator(api) } return api.allocateDefaultContext() } return api } api2go-1.0-RC4/api_test.go000066400000000000000000001520031317567713700153020ustar00rootroot00000000000000package api2go import ( "encoding/json" "errors" "fmt" "net/http" "net/http/httptest" "strconv" "strings" "github.com/manyminds/api2go/jsonapi" "github.com/manyminds/api2go/routing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "gopkg.in/guregu/null.v2" ) const testPrefix = "v1" type requestURLResolver struct { r http.Request calls int } func (m requestURLResolver) GetBaseURL() string { if uri := m.r.Header.Get("REQUEST_URI"); uri != "" { return uri } return "https://example.com" } func (m *requestURLResolver) SetRequest(r http.Request) { m.r = r } type invalid string func (i invalid) GetID() string { return "invalid" } type Post struct { ID string `json:"-"` Title string `json:"title"` Value null.Float `json:"value"` Author *User `json:"-"` Comments []Comment `json:"-"` Bananas []Banana `json:"-"` } func (p Post) GetID() string { return p.ID } func (p *Post) SetID(ID string) error { p.ID = ID return nil } func (p Post) GetReferences() []jsonapi.Reference { return []jsonapi.Reference{ { Name: "author", Type: "users", }, { Name: "comments", Type: "comments", }, { Name: "bananas", Type: "bananas", }, } } func (p Post) GetReferencedIDs() []jsonapi.ReferenceID { result := []jsonapi.ReferenceID{} if p.Author != nil { result = append(result, jsonapi.ReferenceID{ID: p.Author.GetID(), Name: "author", Type: "users"}) } for _, comment := range p.Comments { result = append(result, jsonapi.ReferenceID{ID: comment.GetID(), Name: "comments", Type: "comments"}) } for _, banana := range p.Bananas { result = append(result, jsonapi.ReferenceID{ID: banana.GetID(), Name: "bananas", Type: "bananas"}) } return result } func (p *Post) SetToOneReferenceID(name, ID string) error { if name == "author" { if ID == "" { p.Author = nil } else { p.Author = &User{ID: ID} } return nil } return errors.New("There is no to-one relationship with the name " + name) } func (p *Post) SetToManyReferenceIDs(name string, IDs []string) error { if name == "comments" { comments := []Comment{} for _, ID := range IDs { comments = append(comments, Comment{ID: ID}) } p.Comments = comments return nil } if name == "bananas" { bananas := []Banana{} for _, ID := range IDs { bananas = append(bananas, Banana{ID: ID}) } p.Bananas = bananas return nil } return errors.New("There is no to-many relationship with the name " + name) } func (p *Post) AddToManyIDs(name string, IDs []string) error { if name == "comments" { for _, ID := range IDs { p.Comments = append(p.Comments, Comment{ID: ID}) } } if name == "bananas" { for _, ID := range IDs { p.Bananas = append(p.Bananas, Banana{ID: ID}) } } return errors.New("There is no to-manyrelationship with the name " + name) } func (p *Post) DeleteToManyIDs(name string, IDs []string) error { if name == "comments" { for _, ID := range IDs { // find and delete the comment with ID for pos, comment := range p.Comments { if comment.GetID() == ID { p.Comments = append(p.Comments[:pos], p.Comments[pos+1:]...) } } } } if name == "bananas" { for _, ID := range IDs { // find and delete the comment with ID for pos, banana := range p.Bananas { if banana.GetID() == ID { p.Bananas = append(p.Bananas[:pos], p.Bananas[pos+1:]...) } } } } return errors.New("There is no to-manyrelationship with the name " + name) } func (p Post) GetReferencedStructs() []jsonapi.MarshalIdentifier { result := []jsonapi.MarshalIdentifier{} if p.Author != nil { result = append(result, *p.Author) } for key := range p.Comments { result = append(result, p.Comments[key]) } for key := range p.Bananas { result = append(result, p.Bananas[key]) } return result } type Comment struct { ID string `json:"-"` Value string `json:"value"` } func (c Comment) GetID() string { return c.ID } type Banana struct { ID string `jnson:"-"` Name string } func (b Banana) GetID() string { return b.ID } type User struct { ID string `json:"-"` Name string `json:"name"` Info string `json:"info"` } func (u User) GetID() string { return u.ID } type fixtureSource struct { posts map[string]*Post pointers bool } func (s *fixtureSource) FindAll(req Request) (Responder, error) { var err error if _, ok := req.Pagination["custom"]; ok { return &Response{ Res: []*Post{}, Pagination: Pagination{ Next: map[string]string{"type": "next"}, Prev: map[string]string{"type": "prev"}, First: map[string]string{}, }, }, nil } if limit, ok := req.QueryParams["limit"]; ok { if l, err := strconv.ParseInt(limit[0], 10, 64); err == nil { if s.pointers { postsSlice := make([]*Post, l) length := len(s.posts) for i := 0; i < length; i++ { postsSlice[i] = s.posts[strconv.Itoa(i+1)] if i+1 >= int(l) { break } } return &Response{Res: postsSlice}, nil } postsSlice := make([]Post, l) length := len(s.posts) for i := 0; i < length; i++ { postsSlice[i] = *s.posts[strconv.Itoa(i+1)] if i+1 >= int(l) { break } } return &Response{Res: postsSlice}, nil } fmt.Println("Error casting to int", err) return &Response{}, err } if s.pointers { postsSlice := make([]Post, len(s.posts)) length := len(s.posts) for i := 0; i < length; i++ { postsSlice[i] = *s.posts[strconv.Itoa(i+1)] } return &Response{Res: postsSlice}, nil } postsSlice := make([]*Post, len(s.posts)) length := len(s.posts) for i := 0; i < length; i++ { postsSlice[i] = s.posts[strconv.Itoa(i+1)] } return &Response{Res: postsSlice}, nil } // this does not read the query parameters, which you would do to limit the result in real world usage func (s *fixtureSource) PaginatedFindAll(req Request) (uint, Responder, error) { if s.pointers { postsSlice := []*Post{} for _, post := range s.posts { postsSlice = append(postsSlice, post) } return uint(len(s.posts)), &Response{Res: postsSlice}, nil } postsSlice := []Post{} for _, post := range s.posts { postsSlice = append(postsSlice, *post) } return uint(len(s.posts)), &Response{Res: postsSlice}, nil } func (s *fixtureSource) FindOne(id string, req Request) (Responder, error) { // special test case for nil document if id == "69" { return &Response{Res: nil}, nil } if p, ok := s.posts[id]; ok { if s.pointers { return &Response{Res: p}, nil } return &Response{Res: *p}, nil } return nil, NewHTTPError(nil, "post not found", http.StatusNotFound) } func (s *fixtureSource) Create(obj interface{}, req Request) (Responder, error) { var p *Post if s.pointers { p = obj.(*Post) } else { o := obj.(Post) p = &o } if p.Title == "" { err := NewHTTPError(errors.New("Bad request"), "Bad Request", http.StatusBadRequest) err.Errors = append(err.Errors, Error{ID: "SomeErrorID", Source: &ErrorSource{Pointer: "Title"}}) return &Response{}, err } maxID := 0 for k := range s.posts { id, _ := strconv.Atoi(k) if id > maxID { maxID = id } } newID := strconv.Itoa(maxID + 1) p.ID = newID s.posts[newID] = p return &Response{Res: p, Code: http.StatusCreated}, nil } func (s *fixtureSource) Delete(id string, req Request) (Responder, error) { delete(s.posts, id) return &Response{Code: http.StatusNoContent}, nil } func (s *fixtureSource) Update(obj interface{}, req Request) (Responder, error) { var p *Post if s.pointers { p = obj.(*Post) } else { o := obj.(Post) p = &o } if oldP, ok := s.posts[p.ID]; ok { oldP.Title = p.Title oldP.Author = p.Author oldP.Comments = p.Comments return &Response{Code: http.StatusNoContent}, nil } return &Response{}, NewHTTPError(nil, "post not found", http.StatusNotFound) } type userSource struct { pointers bool } func (s *userSource) FindAll(req Request) (Responder, error) { postsIDs, ok := req.QueryParams["postsID"] if ok { if postsIDs[0] == "1" { u := User{ID: "1", Name: "Dieter"} if s.pointers { return &Response{Res: &u}, nil } return &Response{Res: u}, nil } } if s.pointers { return &Response{}, errors.New("Did not receive query parameter") } return &Response{}, errors.New("Did not receive query parameter") } func (s *userSource) FindOne(id string, req Request) (Responder, error) { return &Response{}, nil } func (s *userSource) Create(obj interface{}, req Request) (Responder, error) { return &Response{Res: obj, Code: http.StatusCreated}, nil } func (s *userSource) Delete(id string, req Request) (Responder, error) { return &Response{Code: http.StatusNoContent}, nil } func (s *userSource) Update(obj interface{}, req Request) (Responder, error) { return &Response{}, NewHTTPError(nil, "user not found", http.StatusNotFound) } type commentSource struct { pointers bool } func (s *commentSource) FindAll(req Request) (Responder, error) { postsIDs, ok := req.QueryParams["postsID"] if ok { if postsIDs[0] == "1" { c := Comment{ ID: "1", Value: "This is a stupid post!", } if s.pointers { return &Response{Res: []*Comment{&c}}, nil } return &Response{Res: []Comment{c}}, nil } } if s.pointers { return &Response{Res: []*Comment{}}, errors.New("Did not receive query parameter") } return &Response{Res: []Comment{}}, errors.New("Did not receive query parameter") } func (s *commentSource) FindOne(id string, req Request) (Responder, error) { return &Response{}, nil } func (s *commentSource) Create(obj interface{}, req Request) (Responder, error) { return &Response{Code: http.StatusCreated, Res: obj}, nil } func (s *commentSource) Delete(id string, req Request) (Responder, error) { return &Response{Code: http.StatusNoContent}, nil } func (s *commentSource) Update(obj interface{}, req Request) (Responder, error) { return &Response{}, NewHTTPError(nil, "comment not found", http.StatusNotFound) } var _ = Describe("RestHandler", func() { var usePointerResources bool requestHandlingTests := func() { var ( source *fixtureSource post1Json map[string]interface{} post1LinkedJSON []map[string]interface{} post2Json map[string]interface{} post3Json map[string]interface{} api *API rec *httptest.ResponseRecorder ) BeforeEach(func() { source = &fixtureSource{map[string]*Post{ "1": { ID: "1", Title: "Hello, World!", Author: &User{ ID: "1", Name: "Dieter", }, Comments: []Comment{{ ID: "1", Value: "This is a stupid post!", }}, }, "2": {ID: "2", Title: "I am NR. 2"}, "3": {ID: "3", Title: "I am NR. 3"}, }, usePointerResources} post1Json = map[string]interface{}{ "id": "1", "type": "posts", "attributes": map[string]interface{}{ "title": "Hello, World!", "value": nil, }, "relationships": map[string]interface{}{ "author": map[string]interface{}{ "data": map[string]interface{}{ "id": "1", "type": "users", }, "links": map[string]string{ "self": "http://localhost/v1/posts/1/relationships/author", "related": "http://localhost/v1/posts/1/author", }, }, "comments": map[string]interface{}{ "data": []map[string]interface{}{ { "id": "1", "type": "comments", }, }, "links": map[string]string{ "self": "http://localhost/v1/posts/1/relationships/comments", "related": "http://localhost/v1/posts/1/comments", }, }, "bananas": map[string]interface{}{ "data": []map[string]interface{}{}, "links": map[string]string{ "self": "http://localhost/v1/posts/1/relationships/bananas", "related": "http://localhost/v1/posts/1/bananas", }, }, }, } post1LinkedJSON = []map[string]interface{}{ { "id": "1", "type": "users", "attributes": map[string]interface{}{ "name": "Dieter", "info": "", }, }, { "id": "1", "type": "comments", "attributes": map[string]interface{}{ "value": "This is a stupid post!", }, }, } post2Json = map[string]interface{}{ "id": "2", "type": "posts", "attributes": map[string]interface{}{ "title": "I am NR. 2", "value": nil, }, "relationships": map[string]interface{}{ "author": map[string]interface{}{ "data": nil, "links": map[string]string{ "self": "http://localhost/v1/posts/2/relationships/author", "related": "http://localhost/v1/posts/2/author", }, }, "comments": map[string]interface{}{ "data": []interface{}{}, "links": map[string]string{ "self": "http://localhost/v1/posts/2/relationships/comments", "related": "http://localhost/v1/posts/2/comments", }, }, "bananas": map[string]interface{}{ "data": []map[string]interface{}{}, "links": map[string]string{ "self": "http://localhost/v1/posts/2/relationships/bananas", "related": "http://localhost/v1/posts/2/bananas", }, }, }, } post3Json = map[string]interface{}{ "id": "3", "type": "posts", "attributes": map[string]interface{}{ "title": "I am NR. 3", "value": nil, }, "relationships": map[string]interface{}{ "author": map[string]interface{}{ "data": nil, "links": map[string]string{ "self": "http://localhost/v1/posts/3/relationships/author", "related": "http://localhost/v1/posts/3/author", }, }, "comments": map[string]interface{}{ "data": []interface{}{}, "links": map[string]string{ "self": "http://localhost/v1/posts/3/relationships/comments", "related": "http://localhost/v1/posts/3/comments", }, }, "bananas": map[string]interface{}{ "data": []map[string]interface{}{}, "links": map[string]string{ "self": "http://localhost/v1/posts/3/relationships/bananas", "related": "http://localhost/v1/posts/3/bananas", }, }, }, } api = NewAPIWithBaseURL("v1", "http://localhost") if usePointerResources { api.AddResource(&Post{}, source) api.AddResource(&User{}, &userSource{true}) api.AddResource(&Comment{}, &commentSource{true}) } else { api.AddResource(Post{}, source) api.AddResource(User{}, &userSource{false}) api.AddResource(Comment{}, &commentSource{false}) } rec = httptest.NewRecorder() }) It("Router() returns a HTTPRouter instance", func() { Expect(api.Router()).To(BeAssignableToTypeOf(&routing.HTTPRouter{})) }) It("GETs collections", func() { req, err := http.NewRequest("GET", "/v1/posts", nil) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) expected, err := json.Marshal(map[string]interface{}{ "data": []map[string]interface{}{post1Json, post2Json, post3Json}, "included": post1LinkedJSON, }) Expect(err).ToNot(HaveOccurred()) Expect(rec.Body.Bytes()).To(MatchJSON(expected)) }) It("GETs single objects", func() { req, err := http.NewRequest("GET", "/v1/posts/1", nil) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) expected, err := json.Marshal(map[string]interface{}{ "data": post1Json, "included": post1LinkedJSON, }) Expect(err).ToNot(HaveOccurred()) Expect(rec.Body.Bytes()).To(MatchJSON(expected)) }) It("GETs single object that is not yet corresponding to a single resource", func() { req, err := http.NewRequest("GET", "/v1/posts/69", nil) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(rec.Body.Bytes()).To(MatchJSON(`{"data": null}`)) }) It("GETs related struct from resource url", func() { req, err := http.NewRequest("GET", "/v1/posts/1/author", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(rec.Body.Bytes()).To(MatchJSON(` {"data": { "id": "1", "type": "users", "attributes": { "name": "Dieter", "info": "" } }}`)) }) It("GETs related structs from resource url", func() { req, err := http.NewRequest("GET", "/v1/posts/1/comments", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(rec.Body.Bytes()).To(MatchJSON(` {"data": [{ "id": "1", "type": "comments", "attributes": { "value": "This is a stupid post!" } }]}`)) }) It("GETs relationship data from relationship url for to-many", func() { req, err := http.NewRequest("GET", "/v1/posts/1/relationships/comments", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(rec.Body.Bytes()).To(MatchJSON(`{"data": [{"id": "1", "type": "comments"}], "links": {"self": "http://localhost/v1/posts/1/relationships/comments", "related": "http://localhost/v1/posts/1/comments"}}`)) }) It("GETs relationship data from relationship url for to-one", func() { req, err := http.NewRequest("GET", "/v1/posts/1/relationships/author", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(rec.Body.Bytes()).To(MatchJSON(`{"data": {"id": "1", "type": "users"}, "links": {"self": "http://localhost/v1/posts/1/relationships/author", "related": "http://localhost/v1/posts/1/author"}}`)) }) It("Gets 404 if a related struct was not found", func() { req, err := http.NewRequest("GET", "/v1/posts/1/unicorns", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNotFound)) Expect(rec.Body.Bytes()).ToNot(BeEmpty()) }) It("404s", func() { req, err := http.NewRequest("GET", "/v1/posts/23", nil) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNotFound)) errorJSON := []byte(`{"errors":[{"status":"404","title":"post not found"}]}`) Expect(rec.Body.Bytes()).To(MatchJSON(errorJSON)) }) It("POSTSs new object", func() { reqBody := strings.NewReader(`{"data": {"attributes":{"title": "New Post" }, "type": "posts"}}`) req, err := http.NewRequest("POST", "/v1/posts", reqBody) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusCreated)) Expect(rec.Header().Get("Location")).To(Equal("/v1/posts/4")) var result map[string]interface{} Expect(json.Unmarshal(rec.Body.Bytes(), &result)).To(BeNil()) Expect(result).To(Equal(map[string]interface{}{ "data": map[string]interface{}{ "id": "4", "type": "posts", "attributes": map[string]interface{}{ "title": "New Post", "value": nil, }, "relationships": map[string]interface{}{ "author": map[string]interface{}{ "data": nil, "links": map[string]interface{}{ "self": "http://localhost/v1/posts/4/relationships/author", "related": "http://localhost/v1/posts/4/author", }, }, "comments": map[string]interface{}{ "data": []interface{}{}, "links": map[string]interface{}{ "self": "http://localhost/v1/posts/4/relationships/comments", "related": "http://localhost/v1/posts/4/comments", }, }, "bananas": map[string]interface{}{ "data": []interface{}{}, "links": map[string]interface{}{ "self": "http://localhost/v1/posts/4/relationships/bananas", "related": "http://localhost/v1/posts/4/bananas", }, }, }, }, })) }) It("Correctly sets Location header without api prefix", func() { api = NewAPI("") if usePointerResources { api.AddResource(&Post{}, source) api.AddResource(&User{}, &userSource{true}) api.AddResource(&Comment{}, &commentSource{true}) } else { api.AddResource(Post{}, source) api.AddResource(User{}, &userSource{false}) api.AddResource(Comment{}, &commentSource{false}) } reqBody := strings.NewReader(`{"data": {"attributes":{"title": "New Post" }, "type": "posts"}}`) req, err := http.NewRequest("POST", "/posts", reqBody) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusCreated)) Expect(rec.Header().Get("Location")).To(Equal("/posts/4")) }) It("POSTSs new objects with trailing slash automatic redirect enabled", func() { reqBody := strings.NewReader(`{"data": [{"title": "New Post", "type": "posts"}]}`) req, err := http.NewRequest("POST", "/v1/posts/", reqBody) Expect(err).To(BeNil()) router := api.Router().(*routing.HTTPRouter) router.SetRedirectTrailingSlash(true) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusTemporaryRedirect)) }) It("POSTSs with client id", func() { reqBody := strings.NewReader(`{"data": {"attributes": {"title": "New Post"}, "id": "100", "type": "posts"}}`) req, err := http.NewRequest("POST", "/v1/posts", reqBody) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusCreated)) }) It("POSTSs new objects with trailing slash automatic redirect disabled", func() { reqBody := strings.NewReader(`{"data": [{"title": "New Post", "type": "posts"}]}`) req, err := http.NewRequest("POST", "/v1/posts/", reqBody) Expect(err).To(BeNil()) router := api.Router().(*routing.HTTPRouter) router.SetRedirectTrailingSlash(false) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNotFound)) }) It("OPTIONS on collection route", func() { req, err := http.NewRequest("OPTIONS", "/v1/posts", nil) api.Handler().ServeHTTP(rec, req) Expect(err).To(BeNil()) Expect(rec.Code).To(Equal(http.StatusNoContent)) Expect(strings.Split(rec.Header().Get("Allow"), ",")).To(Equal([]string{ "OPTIONS", "GET", "PATCH", "POST", })) }) It("OPTIONS on element route", func() { req, err := http.NewRequest("OPTIONS", "/v1/posts/1", nil) api.Handler().ServeHTTP(rec, req) Expect(err).To(BeNil()) Expect(rec.Code).To(Equal(http.StatusNoContent)) Expect(strings.Split(rec.Header().Get("Allow"), ",")).To(Equal([]string{ "OPTIONS", "GET", "PATCH", "DELETE", })) }) It("DELETEs", func() { req, err := http.NewRequest("DELETE", "/v1/posts/1", nil) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNoContent)) Expect(len(source.posts)).To(Equal(2)) }) It("patch must contain type and id but does not have type", func() { reqBody := strings.NewReader(`{"data": {"title": "New Title", "id": "id"}}`) req, err := http.NewRequest("PATCH", "/v1/posts/1", reqBody) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNotAcceptable)) Expect(rec.Body.String()).To(MatchJSON(`{"errors":[{"status":"406","title":"invalid record, no type was specified"}]}`)) }) It("patch must contain type and id but does not have id", func() { reqBody := strings.NewReader(`{"data": {"title": "New Title", "type": "posts"}}`) req, err := http.NewRequest("PATCH", "/v1/posts/1", reqBody) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) // It's up to the user how to implement this. Api2go just checks if the type is correct Expect(rec.Code).To(Equal(http.StatusConflict)) Expect(string(rec.Body.Bytes())).To(MatchJSON(`{"errors":[{"status":"409","title":"id in the resource does not match servers endpoint"}]}`)) }) It("POST without type returns 406", func() { reqBody := strings.NewReader(`{"data": {"title": "New Title"}}`) req, err := http.NewRequest("POST", "/v1/posts", reqBody) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNotAcceptable)) Expect(string(rec.Body.Bytes())).To(MatchJSON(`{"errors":[{"status":"406","title":"invalid record, no type was specified"}]}`)) }) Context("Updating", func() { doRequest := func(payload, url, method string) { reqBody := strings.NewReader(payload) req, err := http.NewRequest(method, url, reqBody) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Body.String()).To(Equal("")) Expect(rec.Code).To(Equal(http.StatusNoContent)) } It("UPDATEs", func() { target := source.posts["1"] target.Value = null.FloatFrom(2) doRequest(`{"data": {"id": "1", "attributes": {"title": "New Title"}, "type": "posts"}}`, "/v1/posts/1", "PATCH") Expect(source.posts["1"].Title).To(Equal("New Title")) Expect(target.Title).To(Equal("New Title")) Expect(target.Value).To(Equal(null.FloatFrom(2))) }) It("Update fails with incorrect id in payload", func() { reqBody := strings.NewReader(`{"data": {"id": "2", "attributes": {"title": "New Title"}, "type": "posts"}}`) req, err := http.NewRequest("PATCH", "/v1/posts/1", reqBody) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusConflict)) Expect(string(rec.Body.Bytes())).To(MatchJSON(`{"errors":[{"status":"409","title":"id in the resource does not match servers endpoint"}]}`)) }) It("UPDATEs correctly using null.* values", func() { target := source.posts["1"] target.Value = null.FloatFrom(2) doRequest(`{"data": {"id": "1", "attributes": {"title": "New Title", "value": null}, "type": "posts"}}`, "/v1/posts/1", "PATCH") Expect(source.posts["1"].Title).To(Equal("New Title")) Expect(target.Title).To(Equal("New Title")) Expect(target.Value.Valid).To(Equal(false)) }) It("Patch updates to-one relationships", func() { target := source.posts["1"] doRequest(`{ "data": { "type": "posts", "id": "1", "attributes": {}, "relationships": { "author": { "data": { "type": "users", "id": "2" } } } } } `, "/v1/posts/1", "PATCH") Expect(target.Author.GetID()).To(Equal("2")) }) It("Patch can delete to-one relationships", func() { target := source.posts["1"] doRequest(`{ "data": { "type": "posts", "id": "1", "attributes": {}, "relationships": { "author": { "data": null } } } } `, "/v1/posts/1", "PATCH") Expect(target.Author).To(BeNil()) }) It("Patch updates to-many relationships", func() { target := source.posts["1"] doRequest(`{ "data": { "type": "posts", "id": "1", "attributes": {}, "relationships": { "comments": { "data": [ { "type": "comments", "id": "2" } ] } } } } `, "/v1/posts/1", "PATCH") Expect(target.Comments[0].GetID()).To(Equal("2")) }) It("Patch can delete to-many relationships", func() { target := source.posts["1"] doRequest(`{ "data": { "type": "posts", "id": "1", "attributes": {}, "relationships": { "comments": { "data": [] } } } } `, "/v1/posts/1", "PATCH") Expect(target.Comments).To(HaveLen(0)) }) It("Relationship PATCH route updates to-one", func() { doRequest(`{ "data": { "type": "users", "id": "2" } }`, "/v1/posts/1/relationships/author", "PATCH") target := source.posts["1"] Expect(target.Author.GetID()).To(Equal("2")) }) It("Relationship PATCH route updates to-many", func() { doRequest(`{ "data": [{ "type": "comments", "id": "2" }] }`, "/v1/posts/1/relationships/comments", "PATCH") target := source.posts["1"] Expect(target.Comments).To(HaveLen(1)) Expect(target.Comments[0].GetID()).To(Equal("2")) }) It("Relationship POST route adds to-many elements", func() { doRequest(`{ "data": [{ "type": "comments", "id": "2" }] }`, "/v1/posts/1/relationships/comments", "POST") target := source.posts["1"] Expect(target.Comments).To(HaveLen(2)) }) It("Relationship DELETE route deletes to-many elements", func() { doRequest(`{ "data": [{ "type": "comments", "id": "1" }] }`, "/v1/posts/1/relationships/comments", "DELETE") target := source.posts["1"] Expect(target.Comments).To(HaveLen(0)) }) }) } usePointerResources = false Context("when handling requests for non-pointer resources", requestHandlingTests) usePointerResources = true Context("when handling requests for pointer resources", requestHandlingTests) Context("marshal errors correctly", func() { var ( source *fixtureSource post1Json map[string]interface{} api *API rec *httptest.ResponseRecorder ) BeforeEach(func() { source = &fixtureSource{map[string]*Post{ "1": {ID: "1", Title: "Hello, World!"}, }, false} post1Json = map[string]interface{}{ "id": "1", "title": "Hello, World!", "value": nil, } api = NewAPI("") api.AddResource(Post{}, source) rec = httptest.NewRecorder() }) It("POSTSs new objects", func() { reqBody := strings.NewReader(`{"data": {"attributes": {"title": ""}, "type": "posts"}}`) req, err := http.NewRequest("POST", "/posts", reqBody) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusBadRequest)) expected := `{"errors":[{"id":"SomeErrorID","source":{"pointer":"Title"}}]}` actual := strings.TrimSpace(string(rec.Body.Bytes())) Expect(actual).To(Equal(expected)) }) }) Context("Extracting query parameters with complete BaseURL API", func() { var ( source *fixtureSource post1JSON map[string]interface{} post2JSON map[string]interface{} api *API rec *httptest.ResponseRecorder ) BeforeEach(func() { source = &fixtureSource{map[string]*Post{ "1": {ID: "1", Title: "Hello, World!"}, "2": {ID: "2", Title: "Hello, from second Post!"}, }, false} post1JSON = map[string]interface{}{ "id": "1", "type": "posts", "attributes": map[string]interface{}{ "title": "Hello, World!", "value": nil, }, "relationships": map[string]interface{}{ "author": map[string]interface{}{ "data": nil, "links": map[string]interface{}{ "self": "http://localhost:1337/v0/posts/1/relationships/author", "related": "http://localhost:1337/v0/posts/1/author", }, }, "bananas": map[string]interface{}{ "data": []interface{}{}, "links": map[string]interface{}{ "self": "http://localhost:1337/v0/posts/1/relationships/bananas", "related": "http://localhost:1337/v0/posts/1/bananas", }, }, "comments": map[string]interface{}{ "data": []interface{}{}, "links": map[string]interface{}{ "self": "http://localhost:1337/v0/posts/1/relationships/comments", "related": "http://localhost:1337/v0/posts/1/comments", }, }, }, } post2JSON = map[string]interface{}{ "id": "2", "type": "posts", "attributes": map[string]interface{}{ "title": "Hello, from second Post!", "value": nil, }, "relationships": map[string]interface{}{ "author": map[string]interface{}{ "data": nil, "links": map[string]interface{}{ "self": "http://localhost:1337/v0/posts/2/relationships/author", "related": "http://localhost:1337/v0/posts/2/author", }, }, "bananas": map[string]interface{}{ "data": []interface{}{}, "links": map[string]interface{}{ "self": "http://localhost:1337/v0/posts/2/relationships/bananas", "related": "http://localhost:1337/v0/posts/2/bananas", }, }, "comments": map[string]interface{}{ "data": []interface{}{}, "links": map[string]interface{}{ "self": "http://localhost:1337/v0/posts/2/relationships/comments", "related": "http://localhost:1337/v0/posts/2/comments", }, }, }, } api = NewAPIWithBaseURL("v0", "http://localhost:1337") api.AddResource(Post{}, source) rec = httptest.NewRecorder() }) It("FindAll returns 2 posts if no limit was set", func() { req, err := http.NewRequest("GET", "/v0/posts", nil) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) var result map[string]interface{} Expect(json.Unmarshal(rec.Body.Bytes(), &result)).To(BeNil()) Expect(result).To(Equal(map[string]interface{}{ "data": []interface{}{post1JSON, post2JSON}, })) }) It("FindAll returns 1 post with limit 1", func() { req, err := http.NewRequest("GET", "/v0/posts?limit=1", nil) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) var result map[string]interface{} Expect(json.Unmarshal(rec.Body.Bytes(), &result)).To(BeNil()) Expect(result).To(Equal(map[string]interface{}{ "data": []interface{}{post1JSON}, })) }) It("Extracts multiple parameters correctly", func() { req, err := http.NewRequest("GET", "/v0/posts?sort=title,date", nil) Expect(err).To(BeNil()) c := &APIContext{} api2goReq := buildRequest(c, req) Expect(api2goReq.QueryParams).To(Equal(map[string][]string{"sort": {"title", "date"}})) }) It("Extracts pagination parameters correctly", func() { req, err := http.NewRequest("GET", "/v0/posts?page[volume]=one&page[size]=10", nil) Expect(err).To(BeNil()) c := &APIContext{} api2goReq := buildRequest(c, req) Expect(api2goReq.Pagination).To(Equal(map[string]string{ "volume": "one", "size": "10", })) }) }) Context("When using pagination", func() { var ( api *API rec *httptest.ResponseRecorder source *fixtureSource ) BeforeEach(func() { source = &fixtureSource{map[string]*Post{ "1": {ID: "1", Title: "Hello, World!"}, "2": {ID: "2", Title: "Hello, World!"}, "3": {ID: "3", Title: "Hello, World!"}, "4": {ID: "4", Title: "Hello, World!"}, "5": {ID: "5", Title: "Hello, World!"}, "6": {ID: "6", Title: "Hello, World!"}, "7": {ID: "7", Title: "Hello, World!"}, }, false} api = NewAPIWithRouting(testPrefix, NewStaticResolver(""), newTestRouter()) api.AddResource(Post{}, source) rec = httptest.NewRecorder() }) // helper function that does a request and returns relevant pagination urls out of the response body doRequest := func(URL string) map[string]string { req, err := http.NewRequest("GET", URL, nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) var response map[string]interface{} Expect(json.Unmarshal(rec.Body.Bytes(), &response)).To(BeNil()) result := map[string]string{} if links, ok := response["links"].(map[string]interface{}); ok { if first, ok := links["first"]; ok { result["first"] = first.(string) Expect(err).ToNot(HaveOccurred()) } if next, ok := links["next"]; ok { result["next"] = next.(string) Expect(err).ToNot(HaveOccurred()) } if prev, ok := links["prev"]; ok { result["prev"] = prev.(string) Expect(err).ToNot(HaveOccurred()) } if last, ok := links["last"]; ok { result["last"] = last.(string) Expect(err).ToNot(HaveOccurred()) } } return result } Context("custom pagination", func() { It("returns the correct links", func() { links := doRequest("/v1/posts?page[custom]=test") Expect(links).To(Equal(map[string]string{ "next": "/v1/posts?page[custom]=test&page[type]=next", "prev": "/v1/posts?page[custom]=test&page[type]=prev", "first": "/v1/posts?page[custom]=test", })) }) }) Context("number & size links", func() { It("No prev and first on first page, size = 1", func() { links := doRequest("/v1/posts?page[number]=1&page[size]=1") Expect(links).To(HaveLen(2)) Expect(links["next"]).To(Equal("/v1/posts?page[number]=2&page[size]=1")) Expect(links["last"]).To(Equal("/v1/posts?page[number]=7&page[size]=1")) }) It("No prev and first on first page, size = 2", func() { links := doRequest("/v1/posts?page[number]=1&page[size]=2") Expect(links).To(HaveLen(2)) Expect(links["next"]).To(Equal("/v1/posts?page[number]=2&page[size]=2")) Expect(links["last"]).To(Equal("/v1/posts?page[number]=4&page[size]=2")) }) It("All links on page 2, size = 2", func() { links := doRequest("/v1/posts?page[number]=2&page[size]=2") Expect(links).To(HaveLen(4)) Expect(links["first"]).To(Equal("/v1/posts?page[number]=1&page[size]=2")) Expect(links["prev"]).To(Equal("/v1/posts?page[number]=1&page[size]=2")) Expect(links["next"]).To(Equal("/v1/posts?page[number]=3&page[size]=2")) Expect(links["last"]).To(Equal("/v1/posts?page[number]=4&page[size]=2")) }) It("No next and last on last page, size = 2", func() { links := doRequest("/v1/posts?page[number]=4&page[size]=2") Expect(links).To(HaveLen(2)) Expect(links["prev"]).To(Equal("/v1/posts?page[number]=3&page[size]=2")) Expect(links["first"]).To(Equal("/v1/posts?page[number]=1&page[size]=2")) }) It("Does not generate links if results fit on one page", func() { links := doRequest("/v1/posts?page[number]=1&page[size]=10") Expect(links).To(HaveLen(0)) }) }) // If the combination of parameters is invalid, no links are generated and the normal FindAll method get's called Context("invalid parameter combinations", func() { It("all 4 of them", func() { links := doRequest("/v1/posts?page[number]=1&page[size]=1&page[offset]=1&page[limit]=1") Expect(links).To(HaveLen(0)) }) It("number only", func() { links := doRequest("/v1/posts?page[number]=1") Expect(links).To(HaveLen(0)) }) It("size only", func() { links := doRequest("/v1/posts?page[size]=1") Expect(links).To(HaveLen(0)) }) It("offset only", func() { links := doRequest("/v1/posts?page[offset]=1") Expect(links).To(HaveLen(0)) }) It("limit only", func() { links := doRequest("/v1/posts?page[limit]=1") Expect(links).To(HaveLen(0)) }) It("number, size & offset", func() { links := doRequest("/v1/posts?page[number]=1&page[size]=1&page[offset]=1") Expect(links).To(HaveLen(0)) }) It("number, size & limit", func() { links := doRequest("/v1/posts?page[number]=1&page[size]=1&page[limit]=1") Expect(links).To(HaveLen(0)) }) It("limit, offset & number", func() { links := doRequest("/v1/posts?page[limit]=1&page[offset]=1&page[number]=1") Expect(links).To(HaveLen(0)) }) It("limit, offset & size", func() { links := doRequest("/v1/posts?page[limit]=1&page[offset]=1&page[size]=1") Expect(links).To(HaveLen(0)) }) }) Context("offset & limit links", func() { It("No prev and first on offset = 0, limit = 1", func() { links := doRequest("/v1/posts?page[offset]=0&page[limit]=1") Expect(links).To(HaveLen(2)) Expect(links["next"]).To(Equal("/v1/posts?page[limit]=1&page[offset]=1")) Expect(links["last"]).To(Equal("/v1/posts?page[limit]=1&page[offset]=6")) }) It("No prev and first on offset = 0, limit = 2", func() { links := doRequest("/v1/posts?page[offset]=0&page[limit]=2") Expect(links).To(HaveLen(2)) Expect(links["next"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=2")) Expect(links["last"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=5")) }) It("All links on offset = 2, limit = 2", func() { links := doRequest("/v1/posts?page[offset]=2&page[limit]=2") Expect(links).To(HaveLen(4)) Expect(links["first"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=0")) Expect(links["prev"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=0")) Expect(links["next"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=4")) Expect(links["last"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=5")) }) It("No next and last on offset = 5, limit = 2", func() { links := doRequest("/v1/posts?page[offset]=5&page[limit]=2") Expect(links).To(HaveLen(2)) Expect(links["prev"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=3")) Expect(links["first"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=0")) }) It("Does not generate links if results fit on one page", func() { links := doRequest("/v1/posts?page[offset]=0&page[limit]=10") Expect(links).To(HaveLen(0)) }) }) Context("error codes", func() { It("Should return the correct header on method not allowed", func() { reqBody := strings.NewReader("") req, err := http.NewRequest("PATCH", "/v1/posts", reqBody) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Header().Get("Content-Type")).To(Equal(defaultContentTypHeader)) Expect(rec.Code).To(Equal(http.StatusMethodNotAllowed)) expected := `{"errors":[{"status":"405","title":"Method Not Allowed"}]}` Expect(rec.Body.String()).To(MatchJSON(expected)) }) }) Context("add resource panics with invalid resources", func() { It("Should really panic", func() { api := NewAPI("blub") invalidDataStructure := new(invalid) testFunc := func() { api.AddResource(*invalidDataStructure, &userSource{}) } Expect(testFunc).To(Panic()) }) }) Context("test utility function getPointerToStruct", func() { type someStruct struct { someEntry string } It("Should work as expected", func() { testItem := someStruct{} actual := getPointerToStruct(testItem) Expect(&testItem).To(Equal(actual)) }) It("should not fail when using a pointer", func() { testItem := &someStruct{} actual := getPointerToStruct(testItem) Expect(&testItem).To(Equal(actual)) }) }) }) Context("When using middleware", func() { var ( api *API rec *httptest.ResponseRecorder source *fixtureSource ) BeforeEach(func() { source = &fixtureSource{map[string]*Post{ "1": {ID: "1", Title: "Hello, World!"}, }, false} api = NewAPIWithRouting(testPrefix, NewStaticResolver(""), newTestRouter()) api.AddResource(Post{}, source) MiddleTest := func(c APIContexter, w http.ResponseWriter, r *http.Request) { w.Header().Add("x-test", "test123") } api.UseMiddleware(MiddleTest) rec = httptest.NewRecorder() }) It("Should call the middleware and set value", func() { rec = httptest.NewRecorder() req, err := http.NewRequest("OPTIONS", "/v1/posts", nil) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Header().Get("x-test")).To(Equal("test123")) }) }) Context("Custom context", func() { var ( api *API customContextCalled bool = false rec *httptest.ResponseRecorder source *fixtureSource ) type CustomContext struct { APIContext } BeforeEach(func() { source = &fixtureSource{map[string]*Post{ "1": {ID: "1", Title: "Hello, World!"}, }, false} api = NewAPIWithRouting(testPrefix, NewStaticResolver(""), newTestRouter()) api.AddResource(Post{}, source) api.SetContextAllocator(func(api *API) APIContexter { customContextCalled = true return &CustomContext{} }) rec = httptest.NewRecorder() }) It("calls into custom context allocator", func() { rec = httptest.NewRecorder() req, err := http.NewRequest("OPTIONS", "/v1/posts", nil) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(customContextCalled).To(BeTrue()) }) }) Context("dynamic baseurl handling", func() { var ( api *API rec *httptest.ResponseRecorder source *fixtureSource ) BeforeEach(func() { source = &fixtureSource{map[string]*Post{ "1": {ID: "1", Title: "Hello, World!"}, }, false} api = NewAPIWithResolver("/secret/", &requestURLResolver{}) api.AddResource(Post{}, source) rec = httptest.NewRecorder() }) It("should change dependening on request header in FindAll", func() { firstURI := "https://god-mode.example.com" secondURI := "https://top-secret.example.com" req, err := http.NewRequest("GET", "/secret/posts", nil) req.Header.Set("REQUEST_URI", firstURI) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(err).ToNot(HaveOccurred()) Expect(rec.Body.Bytes()).To(ContainSubstring(firstURI)) Expect(rec.Body.Bytes()).ToNot(ContainSubstring(secondURI)) rec = httptest.NewRecorder() req2, err := http.NewRequest("GET", "/secret/posts", nil) req2.Header.Set("REQUEST_URI", secondURI) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req2) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(err).ToNot(HaveOccurred()) Expect(rec.Body.Bytes()).To(ContainSubstring(secondURI)) Expect(rec.Body.Bytes()).ToNot(ContainSubstring(firstURI)) }) It("should change dependening on request header in FindOne", func() { expected := "https://god-mode.example.com" req, err := http.NewRequest("GET", "/secret/posts/1", nil) req.Header.Set("REQUEST_URI", expected) Expect(err).To(BeNil()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(err).ToNot(HaveOccurred()) Expect(rec.Body.Bytes()).To(ContainSubstring(expected)) }) }) Context("Sparse Fieldsets", func() { var ( source *fixtureSource api *API rec *httptest.ResponseRecorder ) BeforeEach(func() { author := User{ID: "666", Name: "Tester", Info: "Is curious about testing"} source = &fixtureSource{map[string]*Post{ "1": {ID: "1", Title: "Nice Post", Value: null.FloatFrom(13.37), Author: &author}, }, false} api = NewAPI("") api.AddResource(Post{}, source) rec = httptest.NewRecorder() }) It("only returns requested post fields for single post", func() { req, err := http.NewRequest("GET", "/posts/1?fields[posts]=title,value", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(rec.Body.Bytes()).To(MatchJSON(` {"data": { "id": "1", "type": "posts", "attributes": { "title": "Nice Post", "value": 13.37 }, "relationships": { "author": { "data": { "id": "666", "type": "users" }, "links": { "related": "/posts/1/author", "self": "/posts/1/relationships/author" } }, "bananas": { "data": [], "links": { "related": "/posts/1/bananas", "self": "/posts/1/relationships/bananas" } }, "comments": { "data": [], "links": { "related": "/posts/1/comments", "self": "/posts/1/relationships/comments" } } } }, "included": [ { "attributes": { "info": "Is curious about testing", "name": "Tester" }, "id": "666", "type": "users" } ] }`)) }) It("FindOne: only returns requested post field for single post and includes", func() { req, err := http.NewRequest("GET", "/posts/1?fields[posts]=title&fields[users]=name", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(rec.Body.Bytes()).To(MatchJSON(` {"data": { "id": "1", "type": "posts", "attributes": { "title": "Nice Post" }, "relationships": { "author": { "data": { "id": "666", "type": "users" }, "links": { "related": "/posts/1/author", "self": "/posts/1/relationships/author" } }, "bananas": { "data": [], "links": { "related": "/posts/1/bananas", "self": "/posts/1/relationships/bananas" } }, "comments": { "data": [], "links": { "related": "/posts/1/comments", "self": "/posts/1/relationships/comments" } } } }, "included": [ { "attributes": { "name": "Tester" }, "id": "666", "type": "users" } ] }`)) }) It("FindAll: only returns requested post field for single post and includes", func() { req, err := http.NewRequest("GET", "/posts?fields[posts]=title&fields[users]=name", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(rec.Body.Bytes()).To(MatchJSON(` {"data": [{ "id": "1", "type": "posts", "attributes": { "title": "Nice Post" }, "relationships": { "author": { "data": { "id": "666", "type": "users" }, "links": { "related": "/posts/1/author", "self": "/posts/1/relationships/author" } }, "bananas": { "data": [], "links": { "related": "/posts/1/bananas", "self": "/posts/1/relationships/bananas" } }, "comments": { "data": [], "links": { "related": "/posts/1/comments", "self": "/posts/1/relationships/comments" } } } }], "included": [ { "attributes": { "name": "Tester" }, "id": "666", "type": "users" } ] }`)) }) It("Summarize all invalid field query parameters as error", func() { req, err := http.NewRequest("GET", "/posts?fields[posts]=title,nonexistent&fields[users]=name,title,fluffy,pink", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusBadRequest)) error := HTTPError{} err = json.Unmarshal(rec.Body.Bytes(), &error) Expect(err).ToNot(HaveOccurred()) expectedError := func(field, objType string) Error { return Error{ Status: "Bad Request", Code: codeInvalidQueryFields, Title: fmt.Sprintf(`Field "%s" does not exist for type "%s"`, field, objType), Detail: "Please make sure you do only request existing fields", Source: &ErrorSource{ Parameter: fmt.Sprintf("fields[%s]", objType), }, } } Expect(error.Errors).To(HaveLen(4)) Expect(error.Errors).To(ContainElement(expectedError("nonexistent", "posts"))) Expect(error.Errors).To(ContainElement(expectedError("title", "users"))) Expect(error.Errors).To(ContainElement(expectedError("fluffy", "users"))) Expect(error.Errors).To(ContainElement(expectedError("pink", "users"))) }) }) Context("Works with multiple API verisons", func() { var ( source, source2 *fixtureSource mainAPI, apiV2 *API rec *httptest.ResponseRecorder ) BeforeEach(func() { author := User{ID: "666", Name: "Tester", Info: "Is curious about testing"} source = &fixtureSource{map[string]*Post{ "1": {ID: "1", Title: "Nice Post", Value: null.FloatFrom(13.37), Author: &author}, }, false} mainAPI = NewAPIWithRouting(testPrefix, NewStaticResolver(""), newTestRouter()) mainAPI.AddResource(Post{}, source) author2 := User{ID: "888", Name: "Version 2 Tester", Info: "Is the next version"} source2 = &fixtureSource{map[string]*Post{ "1": {ID: "1", Title: "Even better post", Value: null.FloatFrom(13.37), Author: &author2}, }, false} apiV2 = mainAPI.NewAPIVersion("v2") apiV2.AddResource(Post{}, source2) rec = httptest.NewRecorder() }) It("Works for v1", func() { req, err := http.NewRequest("GET", "/v1/posts", nil) Expect(err).ToNot(HaveOccurred()) mainAPI.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(rec.Body.Bytes()).To(MatchJSON(` { "data": [ { "type": "posts", "id": "1", "attributes": { "title": "Nice Post", "value": 13.37 }, "relationships": { "author": { "links": { "related": "/v1/posts/1/author", "self": "/v1/posts/1/relationships/author" }, "data": { "type": "users", "id": "666" } }, "bananas": { "links": { "related": "/v1/posts/1/bananas", "self": "/v1/posts/1/relationships/bananas" }, "data": [] }, "comments": { "links": { "related": "/v1/posts/1/comments", "self": "/v1/posts/1/relationships/comments" }, "data": [] } } } ], "included": [ { "type": "users", "id": "666", "attributes": { "name": "Tester", "info": "Is curious about testing" } } ] }`)) }) It("Works for v2", func() { req, err := http.NewRequest("GET", "/v2/posts", nil) Expect(err).ToNot(HaveOccurred()) mainAPI.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(rec.Body.Bytes()).To(MatchJSON(` { "data": [ { "type": "posts", "id": "1", "attributes": { "title": "Even better post", "value": 13.37 }, "relationships": { "author": { "links": { "related": "/v2/posts/1/author", "self": "/v2/posts/1/relationships/author" }, "data": { "type": "users", "id": "888" } }, "bananas": { "links": { "related": "/v2/posts/1/bananas", "self": "/v2/posts/1/relationships/bananas" }, "data": [] }, "comments": { "links": { "related": "/v2/posts/1/comments", "self": "/v2/posts/1/relationships/comments" }, "data": [] } } } ], "included": [ { "type": "users", "id": "888", "attributes": { "name": "Version 2 Tester", "info": "Is the next version" } } ] }`)) }) }) }) api2go-1.0-RC4/context.go000066400000000000000000000034011317567713700151530ustar00rootroot00000000000000package api2go import ( "context" "time" ) // APIContextAllocatorFunc to allow custom context implementations type APIContextAllocatorFunc func(*API) APIContexter // APIContexter embedding context.Context and requesting two helper functions type APIContexter interface { context.Context Set(key string, value interface{}) Get(key string) (interface{}, bool) Reset() } // APIContext api2go context for handlers, nil implementations related to Deadline and Done. type APIContext struct { keys map[string]interface{} } // Set a string key value in the context func (c *APIContext) Set(key string, value interface{}) { if c.keys == nil { c.keys = make(map[string]interface{}) } c.keys[key] = value } // Get a key value from the context func (c *APIContext) Get(key string) (value interface{}, exists bool) { if c.keys != nil { value, exists = c.keys[key] } return } // Reset resets all values on Context, making it safe to reuse func (c *APIContext) Reset() { c.keys = nil } // Deadline implements net/context func (c *APIContext) Deadline() (deadline time.Time, ok bool) { return } // Done implements net/context func (c *APIContext) Done() <-chan struct{} { return nil } // Err implements net/context func (c *APIContext) Err() error { return nil } // Value implements net/context func (c *APIContext) Value(key interface{}) interface{} { if keyAsString, ok := key.(string); ok { val, _ := c.Get(keyAsString) return val } return nil } // Compile time check var _ APIContexter = &APIContext{} // ContextQueryParams fetches the QueryParams if Set func ContextQueryParams(c *APIContext) map[string][]string { qp, ok := c.Get("QueryParams") if ok == false { qp = make(map[string][]string) c.Set("QueryParams", qp) } return qp.(map[string][]string) } api2go-1.0-RC4/context_test.go000066400000000000000000000034051317567713700162160ustar00rootroot00000000000000package api2go import ( "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) var _ = Describe("Context", func() { var c *APIContext BeforeEach(func() { c = &APIContext{} }) Context("Set", func() { It("sets key", func() { c.Set("test", 1) _, ok := c.keys["test"] Expect(ok).To(BeTrue()) }) }) Context("Get", func() { BeforeEach(func() { c.Set("test", 2) }) It("gets key", func() { key, ok := c.Get("test") Expect(ok).To(BeTrue()) Expect(key.(int)).To(Equal(2)) }) It("not okay if key does not exist", func() { key, ok := c.Get("nope") Expect(ok).To(BeFalse()) Expect(key).To(BeNil()) }) }) Context("Reset", func() { BeforeEach(func() { c.Set("test", 3) }) It("reset removes keys", func() { c.Reset() _, ok := c.Get("test") Expect(ok).To(BeFalse()) }) }) Context("Not yet implemented", func() { It("Deadline", func() { deadline, ok := c.Deadline() Expect(deadline).To(Equal(time.Time{})) Expect(ok).To(Equal(false)) }) It("Done", func() { var chanel <-chan struct{} Expect(c.Done()).To(Equal(chanel)) }) It("Err", func() { Expect(c.Err()).To(BeNil()) }) }) Context("Value", func() { It("Value returns a set value", func() { c.Set("foo", "bar") Expect(c.Value("foo")).To(Equal("bar")) }) It("Returns nil if key was not a string", func() { Expect(c.Value(1337)).To(BeNil()) }) }) Context("ContextQueryParams", func() { It("returns them if set", func() { queryParams := map[string][]string{ "foo": {"bar"}, } c.Set("QueryParams", queryParams) Expect(ContextQueryParams(c)).To(Equal(queryParams)) }) It("sets empty ones if not set", func() { Expect(ContextQueryParams(c)).To(Equal(map[string][]string{})) }) }) }) api2go-1.0-RC4/doc.go000066400000000000000000000002431317567713700142350ustar00rootroot00000000000000// Package api2go enables building REST servers for the JSONAPI.org standard. // // See https://github.com/manyminds/api2go for usage instructions. package api2go api2go-1.0-RC4/echo_router_test.go000066400000000000000000000013321317567713700170450ustar00rootroot00000000000000// +build echo,!gingonic,!gorillamux package api2go import ( "log" "net/http" "github.com/labstack/echo" "github.com/manyminds/api2go/routing" ) func customHTTPErrorHandler(err error, c echo.Context) { if he, ok := err.(*echo.HTTPError); ok { if he == echo.ErrMethodNotAllowed { handleError(NewHTTPError(he, "Method Not Allowed", http.StatusMethodNotAllowed), c.Response(), c.Request(), defaultContentTypHeader) } } } func newTestRouter() routing.Routeable { e := echo.New() // not found handler, this needs to be fixed as well: see: https://github.com/manyminds/api2go/issues/301 e.HTTPErrorHandler = customHTTPErrorHandler return routing.Echo(e) } func init() { log.Println("Testing with echo router") } api2go-1.0-RC4/error.go000066400000000000000000000045651317567713700146340ustar00rootroot00000000000000package api2go import ( "encoding/json" "fmt" "log" "strconv" ) // HTTPError is used for errors type HTTPError struct { err error msg string status int Errors []Error `json:"errors,omitempty"` } // Error can be used for all kind of application errors // e.g. you would use it to define form errors or any // other semantical application problems // for more information see http://jsonapi.org/format/#errors type Error struct { ID string `json:"id,omitempty"` Links *ErrorLinks `json:"links,omitempty"` Status string `json:"status,omitempty"` Code string `json:"code,omitempty"` Title string `json:"title,omitempty"` Detail string `json:"detail,omitempty"` Source *ErrorSource `json:"source,omitempty"` Meta interface{} `json:"meta,omitempty"` } // ErrorLinks is used to provide an About URL that leads to // further details about the particular occurrence of the problem. // // for more information see http://jsonapi.org/format/#error-objects type ErrorLinks struct { About string `json:"about,omitempty"` } // ErrorSource is used to provide references to the source of an error. // // The Pointer is a JSON Pointer to the associated entity in the request // document. // The Paramter is a string indicating which query parameter caused the error. // // for more information see http://jsonapi.org/format/#error-objects type ErrorSource struct { Pointer string `json:"pointer,omitempty"` Parameter string `json:"parameter,omitempty"` } // marshalHTTPError marshals an internal httpError func marshalHTTPError(input HTTPError) string { if len(input.Errors) == 0 { input.Errors = []Error{{Title: input.msg, Status: strconv.Itoa(input.status)}} } data, err := json.Marshal(input) if err != nil { log.Println(err) return "{}" } return string(data) } // NewHTTPError creates a new error with message and status code. // `err` will be logged (but never sent to a client), `msg` will be sent and `status` is the http status code. // `err` can be nil. func NewHTTPError(err error, msg string, status int) HTTPError { return HTTPError{err: err, msg: msg, status: status} } // Error returns a nice string represenation including the status func (e HTTPError) Error() string { msg := fmt.Sprintf("http error (%d) %s and %d more errors", e.status, e.msg, len(e.Errors)) if e.err != nil { msg += ", " + e.err.Error() } return msg } api2go-1.0-RC4/error_test.go000066400000000000000000000056101317567713700156630ustar00rootroot00000000000000package api2go import ( "errors" "net/http" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) type ErrorMarshaler struct{} func (e ErrorMarshaler) Marshal(i interface{}) ([]byte, error) { return []byte{}, errors.New("this will always fail") } func (e ErrorMarshaler) Unmarshal(data []byte, i interface{}) error { return nil } func (e ErrorMarshaler) MarshalError(error) string { return "" } var _ = Describe("Errors test", func() { Context("validate error logic", func() { It("can create array tree", func() { httpErr := NewHTTPError(errors.New("hi"), "hi", 0) for i := 0; i < 20; i++ { httpErr.Errors = append(httpErr.Errors, Error{}) } Expect(len(httpErr.Errors)).To(Equal(20)) }) }) Context("Marshalling", func() { It("will be marshalled correctly with default error", func() { httpErr := NewHTTPError(nil, "Invalid use case done", http.StatusInternalServerError) result := marshalHTTPError(httpErr) expected := `{"errors":[{"status":"500","title":"Invalid use case done"}]}` Expect(result).To(Equal(expected)) }) It("will be marshalled correctly without child errors", func() { httpErr := NewHTTPError(errors.New("Bad Request"), "Bad Request", 400) result := marshalHTTPError(httpErr) expected := `{"errors":[{"status":"400","title":"Bad Request"}]}` Expect(result).To(Equal(expected)) }) It("will be marshalled correctly with child errors", func() { httpErr := NewHTTPError(errors.New("Bad Request"), "Bad Request", 500) errorOne := Error{ ID: "001", Links: &ErrorLinks{ About: "http://bla/blub", }, Status: "500", Code: "001", Title: "Title must not be empty", Detail: "Never occures in real life", Source: &ErrorSource{ Pointer: "#titleField", }, Meta: map[string]interface{}{ "creator": "api2go", }, } httpErr.Errors = append(httpErr.Errors, errorOne) result := marshalHTTPError(httpErr) expected := `{"errors":[{"id":"001","links":{"about":"http://bla/blub"},"status":"500","code":"001","title":"Title must not be empty","detail":"Never occures in real life","source":{"pointer":"#titleField"},"meta":{"creator":"api2go"}}]}` Expect(result).To(Equal(expected)) }) It("will be marshalled correctly with child errors without links or source", func() { httpErr := NewHTTPError(errors.New("Bad Request"), "Bad Request", 500) errorOne := Error{ ID: "001", Status: "500", Code: "001", Title: "Title must not be empty", Detail: "Never occures in real life", Meta: map[string]interface{}{ "creator": "api2go", }, } httpErr.Errors = append(httpErr.Errors, errorOne) result := marshalHTTPError(httpErr) expected := `{"errors":[{"id":"001","status":"500","code":"001","title":"Title must not be empty","detail":"Never occures in real life","meta":{"creator":"api2go"}}]}` Expect(result).To(Equal(expected)) }) }) }) api2go-1.0-RC4/examples/000077500000000000000000000000001317567713700147605ustar00rootroot00000000000000api2go-1.0-RC4/examples/crud_example.go000066400000000000000000000056461317567713700177720ustar00rootroot00000000000000/* Package examples shows how to implement a basic CRUD for two data structures with the api2go server functionality. To play with this example server you can run some of the following curl requests In order to demonstrate dynamic baseurl handling for requests, apply the --header="REQUEST_URI:https://www.your.domain.example.com" parameter to any of the commands. Create a new user: curl -X POST http://localhost:31415/v0/users -d '{"data" : {"type" : "users" , "attributes": {"user-name" : "marvin"}}}' List users: curl -X GET http://localhost:31415/v0/users List paginated users: curl -X GET 'http://localhost:31415/v0/users?page\[offset\]=0&page\[limit\]=2' OR curl -X GET 'http://localhost:31415/v0/users?page\[number\]=1&page\[size\]=2' Update: curl -vX PATCH http://localhost:31415/v0/users/1 -d '{ "data" : {"type" : "users", "id": "1", "attributes": {"user-name" : "better marvin"}}}' Delete: curl -vX DELETE http://localhost:31415/v0/users/2 Create a chocolate with the name sweet curl -X POST http://localhost:31415/v0/chocolates -d '{"data" : {"type" : "chocolates" , "attributes": {"name" : "Ritter Sport", "taste": "Very Good"}}}' Create a user with a sweet curl -X POST http://localhost:31415/v0/users -d '{"data" : {"type" : "users" , "attributes": {"user-name" : "marvin"}, "relationships": {"sweets": {"data": [{"type": "chocolates", "id": "1"}]}}}}' List a users sweets curl -X GET http://localhost:31415/v0/users/1/sweets Replace a users sweets curl -X PATCH http://localhost:31415/v0/users/1/relationships/sweets -d '{"data" : [{"type": "chocolates", "id": "2"}]}' Add a sweet curl -X POST http://localhost:31415/v0/users/1/relationships/sweets -d '{"data" : [{"type": "chocolates", "id": "2"}]}' Remove a sweet curl -X DELETE http://localhost:31415/v0/users/1/relationships/sweets -d '{"data" : [{"type": "chocolates", "id": "2"}]}' */ package main import ( "fmt" "net/http" "github.com/julienschmidt/httprouter" "github.com/manyminds/api2go" "github.com/manyminds/api2go/examples/model" "github.com/manyminds/api2go/examples/resolver" "github.com/manyminds/api2go/examples/resource" "github.com/manyminds/api2go/examples/storage" ) func main() { port := 31415 api := api2go.NewAPIWithResolver("v0", &resolver.RequestURL{Port: port}) userStorage := storage.NewUserStorage() chocStorage := storage.NewChocolateStorage() api.AddResource(model.User{}, resource.UserResource{ChocStorage: chocStorage, UserStorage: userStorage}) api.AddResource(model.Chocolate{}, resource.ChocolateResource{ChocStorage: chocStorage, UserStorage: userStorage}) fmt.Printf("Listening on :%d", port) handler := api.Handler().(*httprouter.Router) // It is also possible to get the instance of julienschmidt/httprouter and add more custom routes! handler.GET("/hello-world", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, "Hello World!\n") }) http.ListenAndServe(fmt.Sprintf(":%d", port), handler) } api2go-1.0-RC4/examples/crud_example_test.go000066400000000000000000000302411317567713700210160ustar00rootroot00000000000000package main_test import ( "net/http" "net/http/httptest" "strings" "github.com/manyminds/api2go" "github.com/manyminds/api2go/examples/model" "github.com/manyminds/api2go/examples/resource" "github.com/manyminds/api2go/examples/storage" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) // there are a lot of functions because each test can be run individually and sets up the complete // environment. That is because we run all the specs randomized. var _ = Describe("CrudExample", func() { var rec *httptest.ResponseRecorder BeforeEach(func() { api = api2go.NewAPIWithBaseURL("v0", "http://localhost:31415") userStorage := storage.NewUserStorage() chocStorage := storage.NewChocolateStorage() api.AddResource(model.User{}, resource.UserResource{ChocStorage: chocStorage, UserStorage: userStorage}) api.AddResource(model.Chocolate{}, resource.ChocolateResource{ChocStorage: chocStorage, UserStorage: userStorage}) rec = httptest.NewRecorder() }) var createUser = func() { rec = httptest.NewRecorder() req, err := http.NewRequest("POST", "/v0/users", strings.NewReader(` { "data": { "type": "users", "attributes": { "user-name": "marvin" } } } `)) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusCreated)) Expect(rec.Body.String()).To(MatchJSON(` { "meta": { "author": "The api2go examples crew", "license": "wtfpl", "license-url": "http://www.wtfpl.net" }, "data": { "id": "1", "type": "users", "attributes": { "user-name": "marvin" }, "relationships": { "sweets": { "data": [], "links": { "related": "http://localhost:31415/v0/users/1/sweets", "self": "http://localhost:31415/v0/users/1/relationships/sweets" } } } } } `)) } It("Creates a new user", func() { createUser() }) var createChocolate = func() { rec = httptest.NewRecorder() req, err := http.NewRequest("POST", "/v0/chocolates", strings.NewReader(` { "data": { "type": "chocolates", "attributes": { "name": "Ritter Sport", "taste": "Very Good" } } } `)) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusCreated)) Expect(rec.Body.String()).To(MatchJSON(` { "meta": { "author": "The api2go examples crew", "license": "wtfpl", "license-url": "http://www.wtfpl.net" }, "data": { "id": "1", "type": "chocolates", "attributes": { "name": "Ritter Sport", "taste": "Very Good" } } } `)) } It("Creates a new chocolate", func() { createChocolate() }) var replaceSweets = func() { rec = httptest.NewRecorder() By("Replacing sweets relationship with PATCH") req, err := http.NewRequest("PATCH", "/v0/users/1/relationships/sweets", strings.NewReader(` { "data": [{ "type": "chocolates", "id": "1" }] } `)) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNoContent)) By("Loading the user from the backend, it should have the relationship") rec = httptest.NewRecorder() req, err = http.NewRequest("GET", "/v0/users/1", nil) api.Handler().ServeHTTP(rec, req) Expect(err).ToNot(HaveOccurred()) Expect(rec.Body.String()).To(MatchJSON(` { "meta": { "author": "The api2go examples crew", "license": "wtfpl", "license-url": "http://www.wtfpl.net" }, "data": { "attributes": { "user-name": "marvin" }, "id": "1", "relationships": { "sweets": { "data": [ { "id": "1", "type": "chocolates" } ], "links": { "related": "http://localhost:31415/v0/users/1/sweets", "self": "http://localhost:31415/v0/users/1/relationships/sweets" } } }, "type": "users" }, "included": [ { "attributes": { "name": "Ritter Sport", "taste": "Very Good" }, "id": "1", "type": "chocolates" } ] } `)) } It("Creates a user with references chocolates", func() { createChocolate() rec = httptest.NewRecorder() req, err := http.NewRequest("POST", "/v0/users", strings.NewReader(` { "data": { "type": "users", "attributes": { "user-name": "marvin" }, "relationships": { "sweets": { "data": [ { "id": "1", "type": "chocolates" } ] } } } } `)) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusCreated)) Expect(rec.Body.String()).To(MatchJSON(` { "meta": { "author": "The api2go examples crew", "license": "wtfpl", "license-url": "http://www.wtfpl.net" }, "data": { "id": "1", "type": "users", "attributes": { "user-name": "marvin" }, "relationships": { "sweets": { "data": [ { "id": "1", "type": "chocolates" } ], "links": { "related": "http://localhost:31415/v0/users/1/sweets", "self": "http://localhost:31415/v0/users/1/relationships/sweets" } } } } } `)) }) It("Replaces users sweets", func() { createUser() createChocolate() replaceSweets() }) It("Deletes a users sweet", func() { createUser() createChocolate() replaceSweets() rec = httptest.NewRecorder() By("Deleting the users only sweet with ID 1") req, err := http.NewRequest("DELETE", "/v0/users/1/relationships/sweets", strings.NewReader(` { "data": [{ "type": "chocolates", "id": "1" }] } `)) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNoContent)) By("Loading the user from the backend, it should not have any relations") rec = httptest.NewRecorder() req, err = http.NewRequest("GET", "/v0/users/1", nil) api.Handler().ServeHTTP(rec, req) Expect(err).ToNot(HaveOccurred()) Expect(rec.Body.String()).To(MatchJSON(` { "meta": { "author": "The api2go examples crew", "license": "wtfpl", "license-url": "http://www.wtfpl.net" }, "data": { "attributes": { "user-name": "marvin" }, "id": "1", "relationships": { "sweets": { "data": [], "links": { "related": "http://localhost:31415/v0/users/1/sweets", "self": "http://localhost:31415/v0/users/1/relationships/sweets" } } }, "type": "users" } } `)) }) It("Adds a users sweet", func() { createUser() createChocolate() rec = httptest.NewRecorder() By("Adding a sweet with POST") req, err := http.NewRequest("POST", "/v0/users/1/relationships/sweets", strings.NewReader(` { "data": [{ "type": "chocolates", "id": "1" }] } `)) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNoContent)) By("Loading the user from the backend, it should have the relationship") rec = httptest.NewRecorder() req, err = http.NewRequest("GET", "/v0/users/1", nil) api.Handler().ServeHTTP(rec, req) Expect(err).ToNot(HaveOccurred()) Expect(rec.Body.String()).To(MatchJSON(` { "meta": { "author": "The api2go examples crew", "license": "wtfpl", "license-url": "http://www.wtfpl.net" }, "data": { "attributes": { "user-name": "marvin" }, "id": "1", "relationships": { "sweets": { "data": [ { "id": "1", "type": "chocolates" } ], "links": { "related": "http://localhost:31415/v0/users/1/sweets", "self": "http://localhost:31415/v0/users/1/relationships/sweets" } } }, "type": "users" }, "included": [ { "attributes": { "name": "Ritter Sport", "taste": "Very Good" }, "id": "1", "type": "chocolates" } ] } `)) }) Describe("Load sweets of a user directly", func() { BeforeEach(func() { createUser() createChocolate() replaceSweets() rec = httptest.NewRecorder() // add another sweet so we have 2, only 1 is connected with the user req, err := http.NewRequest("POST", "/v0/chocolates", strings.NewReader(` { "data": { "type": "chocolates", "attributes": { "name": "Black Chocolate", "taste": "Bitter" } } } `)) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusCreated)) Expect(rec.Body.String()).To(MatchJSON(` { "meta": { "author": "The api2go examples crew", "license": "wtfpl", "license-url": "http://www.wtfpl.net" }, "data": { "id": "2", "type": "chocolates", "attributes": { "name": "Black Chocolate", "taste": "Bitter" } } } `)) rec = httptest.NewRecorder() }) It("There are 2 chocolates in the datastorage now", func() { req, err := http.NewRequest("GET", "/v0/chocolates", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(rec.Body.String()).To(MatchJSON(` { "meta": { "author": "The api2go examples crew", "license": "wtfpl", "license-url": "http://www.wtfpl.net" }, "data": [ { "attributes": { "name": "Ritter Sport", "taste": "Very Good" }, "id": "1", "type": "chocolates" }, { "attributes": { "name": "Black Chocolate", "taste": "Bitter" }, "id": "2", "type": "chocolates" } ] } `)) }) It("The user only has the previously connected sweet", func() { req, err := http.NewRequest("GET", "/v0/users/1", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(rec.Body.String()).To(MatchJSON(` { "meta": { "author": "The api2go examples crew", "license": "wtfpl", "license-url": "http://www.wtfpl.net" }, "data": { "attributes": { "user-name": "marvin" }, "id": "1", "relationships": { "sweets": { "data": [ { "id": "1", "type": "chocolates" } ], "links": { "related": "http://localhost:31415/v0/users/1/sweets", "self": "http://localhost:31415/v0/users/1/relationships/sweets" } } }, "type": "users" }, "included": [ { "attributes": { "name": "Ritter Sport", "taste": "Very Good" }, "id": "1", "type": "chocolates" } ] } `)) }) It("Directly loading the sweets", func() { req, err := http.NewRequest("GET", "/v0/users/1/sweets", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(rec.Body.String()).To(MatchJSON(` { "meta": { "author": "The api2go examples crew", "license": "wtfpl", "license-url": "http://www.wtfpl.net" }, "data": [ { "type": "chocolates", "id": "1", "attributes": { "name": "Ritter Sport", "taste": "Very Good" } } ] } `)) }) It("The relationship route works too", func() { req, err := http.NewRequest("GET", "/v0/users/1/relationships/sweets", nil) Expect(err).ToNot(HaveOccurred()) api.Handler().ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(rec.Body.String()).To(MatchJSON(` { "meta": { "author": "The api2go examples crew", "license": "wtfpl", "license-url": "http://www.wtfpl.net" }, "data": [ { "id": "1", "type": "chocolates" } ], "links": { "related": "http://localhost:31415/v0/users/1/sweets", "self": "http://localhost:31415/v0/users/1/relationships/sweets" } } `)) }) }) }) api2go-1.0-RC4/examples/examples_suite_test.go000066400000000000000000000003651317567713700214010ustar00rootroot00000000000000package main_test import ( "github.com/manyminds/api2go" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "testing" ) var api *api2go.API func TestExamples(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Examples Suite") } api2go-1.0-RC4/examples/model/000077500000000000000000000000001317567713700160605ustar00rootroot00000000000000api2go-1.0-RC4/examples/model/model_chocolate.go000066400000000000000000000006711317567713700215340ustar00rootroot00000000000000package model // Chocolate is the chocolate that a user consumes in order to get fat and happy type Chocolate struct { ID string `json:"-"` Name string `json:"name"` Taste string `json:"taste"` } // GetID to satisfy jsonapi.MarshalIdentifier interface func (c Chocolate) GetID() string { return c.ID } // SetID to satisfy jsonapi.UnmarshalIdentifier interface func (c *Chocolate) SetID(id string) error { c.ID = id return nil } api2go-1.0-RC4/examples/model/model_user.go000066400000000000000000000050161317567713700205470ustar00rootroot00000000000000package model import ( "errors" "github.com/manyminds/api2go/jsonapi" ) // User is a generic database user type User struct { ID string `json:"-"` //rename the username field to user-name. Username string `json:"user-name"` PasswordHash string `json:"-"` Chocolates []*Chocolate `json:"-"` ChocolatesIDs []string `json:"-"` exists bool } // GetID to satisfy jsonapi.MarshalIdentifier interface func (u User) GetID() string { return u.ID } // SetID to satisfy jsonapi.UnmarshalIdentifier interface func (u *User) SetID(id string) error { u.ID = id return nil } // GetReferences to satisfy the jsonapi.MarshalReferences interface func (u User) GetReferences() []jsonapi.Reference { return []jsonapi.Reference{ { Type: "chocolates", Name: "sweets", }, } } // GetReferencedIDs to satisfy the jsonapi.MarshalLinkedRelations interface func (u User) GetReferencedIDs() []jsonapi.ReferenceID { result := []jsonapi.ReferenceID{} for _, chocolateID := range u.ChocolatesIDs { result = append(result, jsonapi.ReferenceID{ ID: chocolateID, Type: "chocolates", Name: "sweets", }) } return result } // GetReferencedStructs to satisfy the jsonapi.MarhsalIncludedRelations interface func (u User) GetReferencedStructs() []jsonapi.MarshalIdentifier { result := []jsonapi.MarshalIdentifier{} for key := range u.Chocolates { result = append(result, u.Chocolates[key]) } return result } // SetToManyReferenceIDs sets the sweets reference IDs and satisfies the jsonapi.UnmarshalToManyRelations interface func (u *User) SetToManyReferenceIDs(name string, IDs []string) error { if name == "sweets" { u.ChocolatesIDs = IDs return nil } return errors.New("There is no to-many relationship with the name " + name) } // AddToManyIDs adds some new sweets that a users loves so much func (u *User) AddToManyIDs(name string, IDs []string) error { if name == "sweets" { u.ChocolatesIDs = append(u.ChocolatesIDs, IDs...) return nil } return errors.New("There is no to-many relationship with the name " + name) } // DeleteToManyIDs removes some sweets from a users because they made him very sick func (u *User) DeleteToManyIDs(name string, IDs []string) error { if name == "sweets" { for _, ID := range IDs { for pos, oldID := range u.ChocolatesIDs { if ID == oldID { // match, this ID must be removed u.ChocolatesIDs = append(u.ChocolatesIDs[:pos], u.ChocolatesIDs[pos+1:]...) } } } } return errors.New("There is no to-many relationship with the name " + name) } api2go-1.0-RC4/examples/resolver/000077500000000000000000000000001317567713700166215ustar00rootroot00000000000000api2go-1.0-RC4/examples/resolver/resolver.go000066400000000000000000000010641317567713700210120ustar00rootroot00000000000000package resolver import ( "fmt" "net/http" ) //RequestURL simply returns //the request url from REQUEST_URI header //this should not be done in production applications type RequestURL struct { r http.Request Port int } //SetRequest to implement `RequestAwareResolverInterface` func (m *RequestURL) SetRequest(r http.Request) { m.r = r } //GetBaseURL implements `URLResolver` interface func (m RequestURL) GetBaseURL() string { if uri := m.r.Header.Get("REQUEST_URI"); uri != "" { return uri } return fmt.Sprintf("https://localhost:%d", m.Port) } api2go-1.0-RC4/examples/resource/000077500000000000000000000000001317567713700166075ustar00rootroot00000000000000api2go-1.0-RC4/examples/resource/resource_chocolate.go000066400000000000000000000044251317567713700230130ustar00rootroot00000000000000package resource import ( "errors" "net/http" "github.com/manyminds/api2go" "github.com/manyminds/api2go/examples/model" "github.com/manyminds/api2go/examples/storage" ) // ChocolateResource for api2go routes type ChocolateResource struct { ChocStorage *storage.ChocolateStorage UserStorage *storage.UserStorage } // FindAll chocolates func (c ChocolateResource) FindAll(r api2go.Request) (api2go.Responder, error) { usersID, ok := r.QueryParams["usersID"] sweets := c.ChocStorage.GetAll() if ok { // this means that we want to show all sweets of a user, this is the route // /v0/users/1/sweets userID := usersID[0] // filter out sweets with userID, in real world, you would just run a different database query filteredSweets := []model.Chocolate{} user, err := c.UserStorage.GetOne(userID) if err != nil { return &Response{}, err } for _, sweetID := range user.ChocolatesIDs { sweet, err := c.ChocStorage.GetOne(sweetID) if err != nil { return &Response{}, err } filteredSweets = append(filteredSweets, sweet) } return &Response{Res: filteredSweets}, nil } return &Response{Res: sweets}, nil } // FindOne choc func (c ChocolateResource) FindOne(ID string, r api2go.Request) (api2go.Responder, error) { res, err := c.ChocStorage.GetOne(ID) return &Response{Res: res}, err } // Create a new choc func (c ChocolateResource) Create(obj interface{}, r api2go.Request) (api2go.Responder, error) { choc, ok := obj.(model.Chocolate) if !ok { return &Response{}, api2go.NewHTTPError(errors.New("Invalid instance given"), "Invalid instance given", http.StatusBadRequest) } id := c.ChocStorage.Insert(choc) choc.ID = id return &Response{Res: choc, Code: http.StatusCreated}, nil } // Delete a choc :( func (c ChocolateResource) Delete(id string, r api2go.Request) (api2go.Responder, error) { err := c.ChocStorage.Delete(id) return &Response{Code: http.StatusOK}, err } // Update a choc func (c ChocolateResource) Update(obj interface{}, r api2go.Request) (api2go.Responder, error) { choc, ok := obj.(model.Chocolate) if !ok { return &Response{}, api2go.NewHTTPError(errors.New("Invalid instance given"), "Invalid instance given", http.StatusBadRequest) } err := c.ChocStorage.Update(choc) return &Response{Res: choc, Code: http.StatusNoContent}, err } api2go-1.0-RC4/examples/resource/resource_user.go000066400000000000000000000103071317567713700220240ustar00rootroot00000000000000package resource import ( "errors" "net/http" "sort" "strconv" "github.com/manyminds/api2go" "github.com/manyminds/api2go/examples/model" "github.com/manyminds/api2go/examples/storage" ) // UserResource for api2go routes type UserResource struct { ChocStorage *storage.ChocolateStorage UserStorage *storage.UserStorage } // FindAll to satisfy api2go data source interface func (s UserResource) FindAll(r api2go.Request) (api2go.Responder, error) { var result []model.User users := s.UserStorage.GetAll() for _, user := range users { // get all sweets for the user user.Chocolates = []*model.Chocolate{} for _, chocolateID := range user.ChocolatesIDs { choc, err := s.ChocStorage.GetOne(chocolateID) if err != nil { return &Response{}, err } user.Chocolates = append(user.Chocolates, &choc) } result = append(result, *user) } return &Response{Res: result}, nil } // PaginatedFindAll can be used to load users in chunks func (s UserResource) PaginatedFindAll(r api2go.Request) (uint, api2go.Responder, error) { var ( result []model.User number, size, offset, limit string keys []int ) users := s.UserStorage.GetAll() for k := range users { i, err := strconv.ParseInt(k, 10, 64) if err != nil { return 0, &Response{}, err } keys = append(keys, int(i)) } sort.Ints(keys) numberQuery, ok := r.QueryParams["page[number]"] if ok { number = numberQuery[0] } sizeQuery, ok := r.QueryParams["page[size]"] if ok { size = sizeQuery[0] } offsetQuery, ok := r.QueryParams["page[offset]"] if ok { offset = offsetQuery[0] } limitQuery, ok := r.QueryParams["page[limit]"] if ok { limit = limitQuery[0] } if size != "" { sizeI, err := strconv.ParseUint(size, 10, 64) if err != nil { return 0, &Response{}, err } numberI, err := strconv.ParseUint(number, 10, 64) if err != nil { return 0, &Response{}, err } start := sizeI * (numberI - 1) for i := start; i < start+sizeI; i++ { if i >= uint64(len(users)) { break } result = append(result, *users[strconv.FormatInt(int64(keys[i]), 10)]) } } else { limitI, err := strconv.ParseUint(limit, 10, 64) if err != nil { return 0, &Response{}, err } offsetI, err := strconv.ParseUint(offset, 10, 64) if err != nil { return 0, &Response{}, err } for i := offsetI; i < offsetI+limitI; i++ { if i >= uint64(len(users)) { break } result = append(result, *users[strconv.FormatInt(int64(keys[i]), 10)]) } } return uint(len(users)), &Response{Res: result}, nil } // FindOne to satisfy `api2go.DataSource` interface // this method should return the user with the given ID, otherwise an error func (s UserResource) FindOne(ID string, r api2go.Request) (api2go.Responder, error) { user, err := s.UserStorage.GetOne(ID) if err != nil { return &Response{}, api2go.NewHTTPError(err, err.Error(), http.StatusNotFound) } user.Chocolates = []*model.Chocolate{} for _, chocolateID := range user.ChocolatesIDs { choc, err := s.ChocStorage.GetOne(chocolateID) if err != nil { return &Response{}, err } user.Chocolates = append(user.Chocolates, &choc) } return &Response{Res: user}, nil } // Create method to satisfy `api2go.DataSource` interface func (s UserResource) Create(obj interface{}, r api2go.Request) (api2go.Responder, error) { user, ok := obj.(model.User) if !ok { return &Response{}, api2go.NewHTTPError(errors.New("Invalid instance given"), "Invalid instance given", http.StatusBadRequest) } id := s.UserStorage.Insert(user) user.ID = id return &Response{Res: user, Code: http.StatusCreated}, nil } // Delete to satisfy `api2go.DataSource` interface func (s UserResource) Delete(id string, r api2go.Request) (api2go.Responder, error) { err := s.UserStorage.Delete(id) return &Response{Code: http.StatusNoContent}, err } //Update stores all changes on the user func (s UserResource) Update(obj interface{}, r api2go.Request) (api2go.Responder, error) { user, ok := obj.(model.User) if !ok { return &Response{}, api2go.NewHTTPError(errors.New("Invalid instance given"), "Invalid instance given", http.StatusBadRequest) } err := s.UserStorage.Update(user) return &Response{Res: user, Code: http.StatusNoContent}, err } api2go-1.0-RC4/examples/resource/response.go000066400000000000000000000010571317567713700207770ustar00rootroot00000000000000package resource // The Response struct implements api2go.Responder type Response struct { Res interface{} Code int } // Metadata returns additional meta data func (r Response) Metadata() map[string]interface{} { return map[string]interface{}{ "author": "The api2go examples crew", "license": "wtfpl", "license-url": "http://www.wtfpl.net", } } // Result returns the actual payload func (r Response) Result() interface{} { return r.Res } // StatusCode sets the return status code func (r Response) StatusCode() int { return r.Code } api2go-1.0-RC4/examples/storage/000077500000000000000000000000001317567713700164245ustar00rootroot00000000000000api2go-1.0-RC4/examples/storage/storage_chocolate.go000066400000000000000000000035101317567713700224370ustar00rootroot00000000000000package storage import ( "fmt" "sort" "github.com/manyminds/api2go/examples/model" ) // sorting type byID []model.Chocolate func (c byID) Len() int { return len(c) } func (c byID) Swap(i, j int) { c[i], c[j] = c[j], c[i] } func (c byID) Less(i, j int) bool { return c[i].GetID() < c[j].GetID() } // NewChocolateStorage initializes the storage func NewChocolateStorage() *ChocolateStorage { return &ChocolateStorage{make(map[string]*model.Chocolate), 1} } // ChocolateStorage stores all of the tasty chocolate, needs to be injected into // User and Chocolate Resource. In the real world, you would use a database for that. type ChocolateStorage struct { chocolates map[string]*model.Chocolate idCount int } // GetAll of the chocolate func (s ChocolateStorage) GetAll() []model.Chocolate { result := []model.Chocolate{} for key := range s.chocolates { result = append(result, *s.chocolates[key]) } sort.Sort(byID(result)) return result } // GetOne tasty chocolate func (s ChocolateStorage) GetOne(id string) (model.Chocolate, error) { choc, ok := s.chocolates[id] if ok { return *choc, nil } return model.Chocolate{}, fmt.Errorf("Chocolate for id %s not found", id) } // Insert a fresh one func (s *ChocolateStorage) Insert(c model.Chocolate) string { id := fmt.Sprintf("%d", s.idCount) c.ID = id s.chocolates[id] = &c s.idCount++ return id } // Delete one :( func (s *ChocolateStorage) Delete(id string) error { _, exists := s.chocolates[id] if !exists { return fmt.Errorf("Chocolate with id %s does not exist", id) } delete(s.chocolates, id) return nil } // Update updates an existing chocolate func (s *ChocolateStorage) Update(c model.Chocolate) error { _, exists := s.chocolates[c.ID] if !exists { return fmt.Errorf("Chocolate with id %s does not exist", c.ID) } s.chocolates[c.ID] = &c return nil } api2go-1.0-RC4/examples/storage/storage_user.go000066400000000000000000000025511317567713700214600ustar00rootroot00000000000000package storage import ( "errors" "fmt" "net/http" "github.com/manyminds/api2go" "github.com/manyminds/api2go/examples/model" ) // NewUserStorage initializes the storage func NewUserStorage() *UserStorage { return &UserStorage{make(map[string]*model.User), 1} } // UserStorage stores all users type UserStorage struct { users map[string]*model.User idCount int } // GetAll returns the user map (because we need the ID as key too) func (s UserStorage) GetAll() map[string]*model.User { return s.users } // GetOne user func (s UserStorage) GetOne(id string) (model.User, error) { user, ok := s.users[id] if ok { return *user, nil } errMessage := fmt.Sprintf("User for id %s not found", id) return model.User{}, api2go.NewHTTPError(errors.New(errMessage), errMessage, http.StatusNotFound) } // Insert a user func (s *UserStorage) Insert(c model.User) string { id := fmt.Sprintf("%d", s.idCount) c.ID = id s.users[id] = &c s.idCount++ return id } // Delete one :( func (s *UserStorage) Delete(id string) error { _, exists := s.users[id] if !exists { return fmt.Errorf("User with id %s does not exist", id) } delete(s.users, id) return nil } // Update a user func (s *UserStorage) Update(c model.User) error { _, exists := s.users[c.ID] if !exists { return fmt.Errorf("User with id %s does not exist", c.ID) } s.users[c.ID] = &c return nil } api2go-1.0-RC4/gingonic_router_test.go000066400000000000000000000006601317567713700177270ustar00rootroot00000000000000// +build gingonic,!gorillamux,!echo package api2go import ( "log" "github.com/gin-gonic/gin" "github.com/manyminds/api2go/routing" ) func newTestRouter() routing.Routeable { gin.SetMode(gin.ReleaseMode) gg := gin.Default() notFound := func(c *gin.Context) { notAllowedHandler{}.ServeHTTP(c.Writer, c.Request) } gg.NoRoute(notFound) return routing.Gin(gg) } func init() { log.Println("Testing with gin router") } api2go-1.0-RC4/gorillamux_router_test.go000066400000000000000000000005441317567713700203160ustar00rootroot00000000000000// +build !gingonic,!echo,gorillamux package api2go import ( "log" "github.com/gorilla/mux" "github.com/manyminds/api2go/routing" ) func newTestRouter() routing.Routeable { router := mux.NewRouter() router.MethodNotAllowedHandler = notAllowedHandler{} return routing.Gorilla(router) } func init() { log.Println("Testing with gorilla router") } api2go-1.0-RC4/httprouter_test.go000066400000000000000000000004321317567713700167470ustar00rootroot00000000000000// +build !gingonic,!gorillamux,!echo package api2go import ( "log" "github.com/manyminds/api2go/routing" ) func newTestRouter() routing.Routeable { return routing.NewHTTPRouter(testPrefix, ¬AllowedHandler{}) } func init() { log.Println("Testing with default router") } api2go-1.0-RC4/jsonapi/000077500000000000000000000000001317567713700146055ustar00rootroot00000000000000api2go-1.0-RC4/jsonapi/Readme.md000066400000000000000000000006611317567713700163270ustar00rootroot00000000000000# api2go JSONAPI package This package contains [JSON API](http://jsonapi.org) compatible marshal und unmarshal functionality. ``` go get github.com/manyminds/api2go/jsonapi ``` ## Usage For information on how to use this package, please refer to the documentation on the [api2go](https://github.com/manyminds/api2go) main project, the integration_test.go or the [godoc](http://godoc.org/github.com/manyminds/api2go/jsonapi). api2go-1.0-RC4/jsonapi/benchmark_test.go000066400000000000000000000033511317567713700201270ustar00rootroot00000000000000package jsonapi import ( "database/sql" "testing" ) func BenchmarkMarshal(b *testing.B) { post := &Post{ ID: 1, Title: "Title", } for i := 0; i < b.N; i++ { _, err := Marshal(post) if err != nil { panic(err) } } } func BenchmarkUnmarshal(b *testing.B) { post := &Post{ ID: 1, Title: "Title", } data, err := Marshal(post) if err != nil { panic(err) } for i := 0; i < b.N; i++ { err = Unmarshal(data, &Post{}) if err != nil { panic(err) } } } func BenchmarkMarshalSlice(b *testing.B) { post := []Post{ { ID: 1, Title: "Title", }, { ID: 2, Title: "Title", }, } for i := 0; i < b.N; i++ { _, err := Marshal(post) if err != nil { panic(err) } } } func BenchmarkUnmarshalSlice(b *testing.B) { post := []*Post{ { ID: 1, Title: "Title", }, { ID: 2, Title: "Title", }, } data, err := Marshal(post) if err != nil { panic(err) } for i := 0; i < b.N; i++ { var posts []Post err = Unmarshal(data, &posts) if err != nil { panic(err) } } } func BenchmarkMarshalWithRelationships(b *testing.B) { post := &Post{ ID: 1, Title: "Title", AuthorID: sql.NullInt64{Valid: true, Int64: 1}, CommentsIDs: []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, } for i := 0; i < b.N; i++ { _, err := Marshal(post) if err != nil { panic(err) } } } func BenchmarkUnmarshalWithRelationships(b *testing.B) { post := &Post{ ID: 1, Title: "Title", AuthorID: sql.NullInt64{Valid: true, Int64: 1}, CommentsIDs: []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, } data, err := Marshal(post) if err != nil { panic(err) } for i := 0; i < b.N; i++ { err = Unmarshal(data, &Post{}) if err != nil { panic(err) } } } api2go-1.0-RC4/jsonapi/data_structs.go000066400000000000000000000110001317567713700176240ustar00rootroot00000000000000package jsonapi import ( "bytes" "encoding/json" "errors" ) var objectSuffix = []byte("{") var arraySuffix = []byte("[") var stringSuffix = []byte(`"`) // A Document represents a JSON API document as specified here: http://jsonapi.org. type Document struct { Links Links `json:"links,omitempty"` Data *DataContainer `json:"data"` Included []Data `json:"included,omitempty"` Meta map[string]interface{} `json:"meta,omitempty"` } // A DataContainer is used to marshal and unmarshal single objects and arrays // of objects. type DataContainer struct { DataObject *Data DataArray []Data } // UnmarshalJSON unmarshals the JSON-encoded data to the DataObject field if the // root element is an object or to the DataArray field for arrays. func (c *DataContainer) UnmarshalJSON(payload []byte) error { if bytes.HasPrefix(payload, objectSuffix) { return json.Unmarshal(payload, &c.DataObject) } if bytes.HasPrefix(payload, arraySuffix) { return json.Unmarshal(payload, &c.DataArray) } return errors.New("expected a JSON encoded object or array") } // MarshalJSON returns the JSON encoding of the DataArray field or the DataObject // field. It will return "null" if neither of them is set. func (c *DataContainer) MarshalJSON() ([]byte, error) { if c.DataArray != nil { return json.Marshal(c.DataArray) } return json.Marshal(c.DataObject) } // Link represents a link for return in the document. type Link struct { Href string `json:"href"` Meta Meta `json:"meta,omitempty"` } // UnmarshalJSON marshals a string value into the Href field or marshals an // object value into the whole struct. func (l *Link) UnmarshalJSON(payload []byte) error { if bytes.HasPrefix(payload, stringSuffix) { return json.Unmarshal(payload, &l.Href) } if bytes.HasPrefix(payload, objectSuffix) { obj := make(map[string]interface{}) err := json.Unmarshal(payload, &obj) if err != nil { return err } var ok bool l.Href, ok = obj["href"].(string) if !ok { return errors.New(`link object expects a "href" key`) } l.Meta, _ = obj["meta"].(map[string]interface{}) return nil } return errors.New("expected a JSON encoded string or object") } // MarshalJSON returns the JSON encoding of only the Href field if the Meta // field is empty, otherwise it marshals the whole struct. func (l Link) MarshalJSON() ([]byte, error) { if len(l.Meta) == 0 { return json.Marshal(l.Href) } return json.Marshal(map[string]interface{}{ "href": l.Href, "meta": l.Meta, }) } // Links contains a map of custom Link objects as given by an element. type Links map[string]Link // Meta contains unstructured metadata type Meta map[string]interface{} // Data is a general struct for document data and included data. type Data struct { Type string `json:"type"` ID string `json:"id"` Attributes json.RawMessage `json:"attributes"` Relationships map[string]Relationship `json:"relationships,omitempty"` Links Links `json:"links,omitempty"` } // Relationship contains reference IDs to the related structs type Relationship struct { Links Links `json:"links,omitempty"` Data *RelationshipDataContainer `json:"data,omitempty"` Meta map[string]interface{} `json:"meta,omitempty"` } // A RelationshipDataContainer is used to marshal and unmarshal single relationship // objects and arrays of relationship objects. type RelationshipDataContainer struct { DataObject *RelationshipData DataArray []RelationshipData } // UnmarshalJSON unmarshals the JSON-encoded data to the DataObject field if the // root element is an object or to the DataArray field for arrays. func (c *RelationshipDataContainer) UnmarshalJSON(payload []byte) error { if bytes.HasPrefix(payload, objectSuffix) { // payload is an object return json.Unmarshal(payload, &c.DataObject) } if bytes.HasPrefix(payload, arraySuffix) { // payload is an array return json.Unmarshal(payload, &c.DataArray) } return errors.New("Invalid json for relationship data array/object") } // MarshalJSON returns the JSON encoding of the DataArray field or the DataObject // field. It will return "null" if neither of them is set. func (c *RelationshipDataContainer) MarshalJSON() ([]byte, error) { if c.DataArray != nil { return json.Marshal(c.DataArray) } return json.Marshal(c.DataObject) } // RelationshipData represents one specific reference ID. type RelationshipData struct { Type string `json:"type"` ID string `json:"id"` } api2go-1.0-RC4/jsonapi/data_structs_test.go000066400000000000000000000123531317567713700206770ustar00rootroot00000000000000package jsonapi import ( "encoding/json" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) var _ = Describe("JSONAPI Struct tests", func() { Context("Testing array and object data payload", func() { It("detects object payload", func() { sampleJSON := `{ "data": { "type": "test", "id": "1", "attributes": {"foo": "bar"}, "relationships": { "author": { "data": {"type": "author", "id": "1"} } } } }` expectedData := &Data{ Type: "test", ID: "1", Attributes: json.RawMessage([]byte(`{"foo": "bar"}`)), Relationships: map[string]Relationship{ "author": { Data: &RelationshipDataContainer{ DataObject: &RelationshipData{ Type: "author", ID: "1", }, }, }, }, } target := Document{} err := json.Unmarshal([]byte(sampleJSON), &target) Expect(err).ToNot(HaveOccurred()) Expect(target.Data.DataObject).To(Equal(expectedData)) }) It("detects array payload", func() { sampleJSON := `{ "data": [ { "type": "test", "id": "1", "attributes": {"foo": "bar"}, "relationships": { "comments": { "data": [ {"type": "comments", "id": "1"}, {"type": "comments", "id": "2"} ] } } } ] }` expectedData := Data{ Type: "test", ID: "1", Attributes: json.RawMessage([]byte(`{"foo": "bar"}`)), Relationships: map[string]Relationship{ "comments": { Data: &RelationshipDataContainer{ DataArray: []RelationshipData{ { Type: "comments", ID: "1", }, { Type: "comments", ID: "2", }, }, }, }, }, } target := Document{} err := json.Unmarshal([]byte(sampleJSON), &target) Expect(err).ToNot(HaveOccurred()) Expect(target.Data.DataArray).To(Equal([]Data{expectedData})) }) }) It("return an error for invalid relationship data format", func() { sampleJSON := ` { "data": [ { "type": "test", "id": "1", "attributes": {"foo": "bar"}, "relationships": { "comments": { "data": "foo" } } } ] }` target := Document{} err := json.Unmarshal([]byte(sampleJSON), &target) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("Invalid json for relationship data array/object")) }) It("creates an empty slice for empty to-many relationships and nil for empty toOne", func() { sampleJSON := `{ "data": [ { "type": "test", "id": "1", "attributes": {"foo": "bar"}, "relationships": { "comments": { "data": [] }, "author": { "data": null } } } ] }` expectedData := Data{ Type: "test", ID: "1", Attributes: json.RawMessage([]byte(`{"foo": "bar"}`)), Relationships: map[string]Relationship{ "comments": { Data: &RelationshipDataContainer{ DataArray: []RelationshipData{}, }, }, "author": { Data: nil, }, }, } target := Document{} err := json.Unmarshal([]byte(sampleJSON), &target) Expect(err).ToNot(HaveOccurred()) Expect(target.Data.DataArray).To(Equal([]Data{expectedData})) }) Context("Marshal and Unmarshal link structs", func() { It("marshals to a string with no metadata", func() { link := Link{Href: "test link"} ret, err := json.Marshal(&link) Expect(err).ToNot(HaveOccurred()) Expect(ret).To(MatchJSON(`"test link"`)) }) It("marshals to an object with metadata", func() { link := Link{ Href: "test link", Meta: map[string]interface{}{ "test": "data", }, } ret, err := json.Marshal(&link) Expect(err).ToNot(HaveOccurred()) Expect(ret).To(MatchJSON(`{ "href": "test link", "meta": {"test": "data"} }`)) }) It("unmarshals from a string", func() { expected := Link{Href: "test link"} target := Link{} err := json.Unmarshal([]byte(`"test link"`), &target) Expect(err).ToNot(HaveOccurred()) Expect(target).To(Equal(expected)) }) It("unmarshals from an object", func() { expected := Link{ Href: "test link", Meta: Meta{ "test": "data", }, } target := Link{} err := json.Unmarshal([]byte(`{ "href": "test link", "meta": {"test": "data"} }`), &target) Expect(err).ToNot(HaveOccurred()) Expect(target).To(Equal(expected)) }) It("unmarshals with an error when href is missing", func() { err := json.Unmarshal([]byte(`{}`), &Link{}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal(`link object expects a "href" key`)) }) It("unmarshals with an error for syntax error", func() { badPayloads := []string{`{`, `"`} for _, payload := range badPayloads { err := json.Unmarshal([]byte(payload), &Link{}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("unexpected end of JSON input")) } }) It("unmarshals with an error for wrong types", func() { badPayloads := []string{`null`, `13`, `[]`} for _, payload := range badPayloads { err := json.Unmarshal([]byte(payload), &Link{}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("expected a JSON encoded string or object")) } }) }) }) api2go-1.0-RC4/jsonapi/entity_namer.go000066400000000000000000000004041317567713700176300ustar00rootroot00000000000000package jsonapi // The EntityNamer interface can be optionally implemented to directly return the // name of resource used for the "type" field. // // Note: By default the name is guessed from the struct name. type EntityNamer interface { GetName() string } api2go-1.0-RC4/jsonapi/fixtures_test.go000066400000000000000000000311321317567713700200440ustar00rootroot00000000000000package jsonapi import ( "database/sql" "errors" "fmt" "strconv" "time" "gopkg.in/guregu/null.v2/zero" ) type Magic struct { ID MagicID `json:"-"` } func (m Magic) GetID() string { return m.ID.String() } type MagicID string func (m MagicID) String() string { return "This should be visible" } type Comment struct { ID int `json:"-"` Text string `json:"text"` SubComments []Comment `json:"-"` SubCommentsEmpty bool `json:"-"` } func (c Comment) GetID() string { return fmt.Sprintf("%d", c.ID) } func (c *Comment) SetID(stringID string) error { id, err := strconv.Atoi(stringID) if err != nil { return err } c.ID = id return nil } func (c Comment) GetReferences() []Reference { return []Reference{ { Type: "comments", Name: "comments", IsNotLoaded: c.SubCommentsEmpty, }, } } func (c Comment) GetReferencedIDs() []ReferenceID { result := []ReferenceID{} for _, comment := range c.SubComments { commentID := ReferenceID{Type: "comments", Name: "comments", ID: comment.GetID()} result = append(result, commentID) } return result } func (c Comment) GetReferencedStructs() []MarshalIdentifier { result := []MarshalIdentifier{} for _, comment := range c.SubComments { result = append(result, comment) } return result } type User struct { ID int `json:"-"` Name string `json:"name"` Password string `json:"-"` } func (u User) GetID() string { return fmt.Sprintf("%d", u.ID) } func (u *User) SetID(stringID string) error { id, err := strconv.Atoi(stringID) if err != nil { return err } u.ID = id return nil } type SimplePost struct { ID string `json:"-"` Title string `json:"title"` Text string `json:"text"` Internal string `json:"-"` Size int `json:"size"` Created time.Time `json:"created-date"` Updated time.Time `json:"updated-date"` topSecret string } func (s SimplePost) GetID() string { return s.ID } func (s *SimplePost) SetID(ID string) error { s.ID = ID return nil } type ErrorIDPost struct { Error error } func (s ErrorIDPost) GetID() string { return "" } func (s *ErrorIDPost) SetID(ID string) error { return s.Error } type Post struct { ID int `json:"-"` Title string `json:"title"` Comments []Comment `json:"-"` CommentsIDs []int `json:"-"` CommentsEmpty bool `json:"-"` Author *User `json:"-"` AuthorID sql.NullInt64 `json:"-"` AuthorEmpty bool `json:"-"` } func (c Post) GetID() string { return fmt.Sprintf("%d", c.ID) } func (c *Post) SetID(stringID string) error { id, err := strconv.Atoi(stringID) if err != nil { return err } c.ID = id return nil } func (c Post) GetReferences() []Reference { return []Reference{ { Type: "comments", Name: "comments", IsNotLoaded: c.CommentsEmpty, }, { Type: "users", Name: "author", IsNotLoaded: c.AuthorEmpty, }, } } func (c *Post) SetToOneReferenceID(name, ID string) error { if name == "author" { intID, err := strconv.ParseInt(ID, 10, 64) if err != nil { return err } c.AuthorID = sql.NullInt64{Valid: true, Int64: intID} return nil } return errors.New("There is no to-one relationship named " + name) } func (c *Post) SetToManyReferenceIDs(name string, IDs []string) error { if name == "comments" { commentsIDs := []int{} for _, ID := range IDs { intID, err := strconv.ParseInt(ID, 10, 64) if err != nil { return err } commentsIDs = append(commentsIDs, int(intID)) } c.CommentsIDs = commentsIDs return nil } return errors.New("There is no to-many relationship named " + name) } func (c *Post) SetReferencedIDs(ids []ReferenceID) error { for _, reference := range ids { intID, err := strconv.ParseInt(reference.ID, 10, 64) if err != nil { return err } switch reference.Name { case "comments": c.CommentsIDs = append(c.CommentsIDs, int(intID)) case "author": c.AuthorID = sql.NullInt64{Valid: true, Int64: intID} } } return nil } func (c Post) GetReferencedIDs() []ReferenceID { result := []ReferenceID{} if c.Author != nil { authorID := ReferenceID{Type: "users", Name: "author", ID: c.Author.GetID()} result = append(result, authorID) } else if c.AuthorID.Valid { authorID := ReferenceID{Type: "users", Name: "author", ID: fmt.Sprintf("%d", c.AuthorID.Int64)} result = append(result, authorID) } if len(c.Comments) > 0 { for _, comment := range c.Comments { result = append(result, ReferenceID{Type: "comments", Name: "comments", ID: comment.GetID()}) } } else if len(c.CommentsIDs) > 0 { for _, commentID := range c.CommentsIDs { result = append(result, ReferenceID{Type: "comments", Name: "comments", ID: fmt.Sprintf("%d", commentID)}) } } return result } func (c Post) GetReferencedStructs() []MarshalIdentifier { result := []MarshalIdentifier{} if c.Author != nil { result = append(result, c.Author) } for key := range c.Comments { result = append(result, c.Comments[key]) } return result } func (c *Post) SetReferencedStructs(references []UnmarshalIdentifier) error { return nil } type AnotherPost struct { ID int `json:"-"` AuthorID int `json:"-"` Author *User `json:"-"` } func (p AnotherPost) GetID() string { return fmt.Sprintf("%d", p.ID) } func (p AnotherPost) GetReferences() []Reference { return []Reference{ { Type: "users", Name: "author", }, } } func (p AnotherPost) GetReferencedIDs() []ReferenceID { result := []ReferenceID{} if p.AuthorID != 0 { result = append(result, ReferenceID{ID: fmt.Sprintf("%d", p.AuthorID), Name: "author", Type: "users"}) } return result } type ZeroPost struct { ID string `json:"-"` Title string `json:"title"` Value zero.Float `json:"value"` } func (z ZeroPost) GetID() string { return z.ID } type ZeroPostPointer struct { ID string `json:"-"` Title string `json:"title"` Value *zero.Float `json:"value"` } func (z ZeroPostPointer) GetID() string { return z.ID } type Question struct { ID string `json:"-"` Text string `json:"text"` InspiringQuestionID sql.NullString `json:"-"` InspiringQuestion *Question `json:"-"` } func (q Question) GetID() string { return q.ID } func (q Question) GetReferences() []Reference { return []Reference{ { Type: "questions", Name: "inspiringQuestion", }, } } func (q Question) GetReferencedIDs() []ReferenceID { result := []ReferenceID{} if q.InspiringQuestionID.Valid { result = append(result, ReferenceID{ID: q.InspiringQuestionID.String, Name: "inspiringQuestion", Type: "questions"}) } return result } func (q Question) GetReferencedStructs() []MarshalIdentifier { result := []MarshalIdentifier{} if q.InspiringQuestion != nil { result = append(result, *q.InspiringQuestion) } return result } type Identity struct { ID int64 `json:"-"` Scopes []string `json:"scopes"` } func (i Identity) GetID() string { return fmt.Sprintf("%d", i.ID) } func (i *Identity) SetID(ID string) error { var err error i.ID, err = strconv.ParseInt(ID, 10, 64) return err } type Unicorn struct { UnicornID int64 `json:"unicorn_id"` // annotations are ignored Scopes []string `json:"scopes"` } func (u Unicorn) GetID() string { return "magicalUnicorn" } type NumberPost struct { ID string `json:"-"` Title string Number int64 UnsignedNumber uint64 } func (n *NumberPost) SetID(ID string) error { n.ID = ID return nil } type SQLNullPost struct { ID string `json:"-"` Title zero.String `json:"title"` Likes zero.Int `json:"likes"` Rating zero.Float `json:"rating"` IsCool zero.Bool `json:"isCool"` Today zero.Time `json:"today"` } func (s SQLNullPost) GetID() string { return s.ID } func (s *SQLNullPost) SetID(ID string) error { s.ID = ID return nil } type RenamedPostWithEmbedding struct { Embedded SQLNullPost ID string `json:"-"` Another string `json:"another"` Field string `json:"foo"` Other string `json:"bar-bar"` Ignored string `json:"-"` } func (p *RenamedPostWithEmbedding) SetID(ID string) error { p.ID = ID return nil } func (s SQLNullPost) GetName() string { return "sqlNullPosts" } type RenamedComment struct { Data string } func (r RenamedComment) GetID() string { return "666" } func (r RenamedComment) GetName() string { return "renamed-comments" } type CompleteServerInformation struct{} const baseURL = "http://my.domain" const prefix = "v1" func (i CompleteServerInformation) GetBaseURL() string { return baseURL } func (i CompleteServerInformation) GetPrefix() string { return prefix } type BaseURLServerInformation struct{} func (i BaseURLServerInformation) GetBaseURL() string { return baseURL } func (i BaseURLServerInformation) GetPrefix() string { return "" } type PrefixServerInformation struct{} func (i PrefixServerInformation) GetBaseURL() string { return "" } func (i PrefixServerInformation) GetPrefix() string { return prefix } type CustomLinksPost struct{} func (n CustomLinksPost) GetID() string { return "someID" } func (n *CustomLinksPost) SetID(ID string) error { return nil } func (n CustomLinksPost) GetName() string { return "posts" } func (n CustomLinksPost) GetCustomLinks(base string) Links { return Links{ "someLink": Link{Href: base + `/someLink`}, "otherLink": Link{ Href: base + `/otherLink`, Meta: Meta{ "method": "GET", }, }, } } type CustomMetaPost struct{} func (n CustomMetaPost) GetID() string { return "someID" } func (n *CustomMetaPost) SetID(ID string) error { return nil } func (n CustomMetaPost) GetName() string { return "posts" } func (n CustomMetaPost) GetReferences() []Reference { return []Reference{ { Type: "users", Name: "author", IsNotLoaded: true, }, } } func (n CustomMetaPost) GetReferencedIDs() []ReferenceID { return nil } func (n CustomMetaPost) GetCustomMeta(linkURL string) map[string]Meta { meta := map[string]Meta{ "author": { "someMetaKey": "someMetaValue", "someOtherMetaKey": "someOtherMetaValue", }, } return meta } type NoRelationshipPosts struct{} func (n NoRelationshipPosts) GetID() string { return "someID" } func (n *NoRelationshipPosts) SetID(ID string) error { return nil } func (n NoRelationshipPosts) GetName() string { return "posts" } type ErrorRelationshipPosts struct{} func (e ErrorRelationshipPosts) GetID() string { return "errorID" } func (e *ErrorRelationshipPosts) SetID(ID string) error { return nil } func (e ErrorRelationshipPosts) GetName() string { return "posts" } func (e ErrorRelationshipPosts) SetToOneReferenceID(name, ID string) error { return errors.New("this never works") } func (e ErrorRelationshipPosts) SetToManyReferenceIDs(name string, IDs []string) error { return errors.New("this also never works") } type Image struct { ID string `json:"-"` Ports []ImagePort `json:"image-ports"` } func (i Image) GetID() string { return i.ID } func (i *Image) SetID(ID string) error { i.ID = ID return nil } type ImagePort struct { Protocol string `json:"protocol"` Number int `json:"number"` } type Article struct { IDs []string `json:"-"` Type string `json:"-"` Name string `json:"-"` Relationship RelationshipType `json:"-"` } func (a Article) GetID() string { return "id" } func (a Article) GetReferences() []Reference { return []Reference{{Type: a.Type, Name: a.Name, Relationship: a.Relationship}} } func (a Article) GetReferencedIDs() []ReferenceID { referenceIDs := []ReferenceID{} for _, id := range a.IDs { referenceIDs = append(referenceIDs, ReferenceID{ID: id, Type: a.Type, Name: a.Name, Relationship: a.Relationship}) } return referenceIDs } type DeepDedendencies struct { ID string `json:"-"` Relationships []DeepDedendencies `json:"-"` } func (d DeepDedendencies) GetID() string { return d.ID } func (DeepDedendencies) GetName() string { return "deep" } func (d DeepDedendencies) GetReferences() []Reference { return []Reference{{Type: "deep", Name: "deps"}} } func (d DeepDedendencies) GetReferencedIDs() []ReferenceID { references := make([]ReferenceID, 0, len(d.Relationships)) for _, r := range d.Relationships { references = append(references, ReferenceID{ID: r.ID, Type: "deep", Name: "deps"}) } return references } func (d DeepDedendencies) GetReferencedStructs() []MarshalIdentifier { var structs []MarshalIdentifier for _, r := range d.Relationships { structs = append(structs, r) structs = append(structs, r.GetReferencedStructs()...) } return structs } api2go-1.0-RC4/jsonapi/helpers.go000066400000000000000000000021411317567713700165740ustar00rootroot00000000000000package jsonapi import ( "strings" "unicode" "github.com/gedex/inflector" ) // https://github.com/golang/lint/blob/3d26dc39376c307203d3a221bada26816b3073cf/lint.go#L482 var commonInitialisms = map[string]bool{ "API": true, "ASCII": true, "CPU": true, "CSS": true, "DNS": true, "EOF": true, "GUID": true, "HTML": true, "HTTP": true, "HTTPS": true, "ID": true, "IP": true, "JSON": true, "LHS": true, "QPS": true, "RAM": true, "RHS": true, "RPC": true, "SLA": true, "SMTP": true, "SSH": true, "TLS": true, "TTL": true, "UI": true, "UID": true, "UUID": true, "URI": true, "URL": true, "UTF8": true, "VM": true, "XML": true, "JWT": true, } // Jsonify returns a JSON formatted key name from a go struct field name. func Jsonify(s string) string { if s == "" { return "" } if commonInitialisms[s] { return strings.ToLower(s) } rs := []rune(s) rs[0] = unicode.ToLower(rs[0]) return string(rs) } // Pluralize returns the pluralization of a noun. func Pluralize(word string) string { return inflector.Pluralize(word) } api2go-1.0-RC4/jsonapi/helpers_test.go000066400000000000000000000010671317567713700176410ustar00rootroot00000000000000package jsonapi import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) var _ = Describe("StringHelpers", func() { Context("json funcs", func() { It("Pluralizes", func() { Expect(Pluralize("post")).To(Equal("posts")) Expect(Pluralize("posts")).To(Equal("posts")) Expect(Pluralize("category")).To(Equal("categories")) }) Context("Jsonify", func() { It("handles empty strings", func() { Expect(Jsonify("")).To(Equal("")) }) It("uses common initialisms", func() { Expect(Jsonify("RAM")).To(Equal("ram")) }) }) }) }) api2go-1.0-RC4/jsonapi/integration_test.go000066400000000000000000000102141317567713700205140ustar00rootroot00000000000000package jsonapi import ( "errors" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) type Book struct { ID string `json:"-"` Author *StupidUser `json:"-"` AuthorID string `json:"-"` Pages []Page `json:"-"` PagesIDs []string `json:"-"` } func (b Book) GetID() string { return b.ID } func (b *Book) SetID(ID string) error { b.ID = ID return nil } func (b Book) GetReferences() []Reference { return []Reference{ { Type: "stupidUsers", Name: "author", }, { Type: "pages", Name: "pages", }, } } func (b Book) GetReferencedIDs() []ReferenceID { result := []ReferenceID{} if b.Author != nil { result = append(result, ReferenceID{ ID: b.Author.GetID(), Name: "author", Type: "stupidUsers", }) } for _, page := range b.Pages { result = append(result, ReferenceID{ ID: page.GetID(), Name: "pages", Type: "pages", }) } return result } func (b *Book) SetToOneReferenceID(name, ID string) error { if name == "author" { b.AuthorID = ID return nil } return errors.New("There is no to-one relationship with name " + name) } func (b *Book) SetToManyReferenceIDs(name string, IDs []string) error { if name == "pages" { b.PagesIDs = IDs return nil } return errors.New("There is no to-many relationship with name " + name) } func (b Book) GetReferencedStructs() []MarshalIdentifier { result := []MarshalIdentifier{} if b.Author != nil { result = append(result, *b.Author) } for key := range b.Pages { result = append(result, b.Pages[key]) } return result } type StupidUser struct { ID string `json:"-"` Name string `json:"name"` } func (s StupidUser) GetID() string { return s.ID } type Page struct { ID string `json:"-"` Content string `json:"content"` } func (p Page) GetID() string { return p.ID } var _ = Describe("Test for the public api of this package", func() { author := StupidUser{ ID: "A Magical UserID", Name: "Terry Pratchett", } pages := []Page{ {ID: "Page 1", Content: "First Page"}, {ID: "Page 2", Content: "Second Page"}, {ID: "Page 3", Content: "Final page"}, } testBook := Book{ ID: "TheOneAndOnlyID", Author: &author, Pages: pages, } testResult := `{ "data": { "id": "TheOneAndOnlyID", "attributes": {}, "relationships": { "author": { "data": { "id" : "A Magical UserID", "type" : "stupidUsers" } }, "pages": { "data": [ { "id": "Page 1", "type": "pages" }, { "id": "Page 2", "type": "pages" }, { "id": "Page 3", "type": "pages" } ] } }, "type": "books" }, "included": [ { "id" : "A Magical UserID", "attributes": { "name" : "Terry Pratchett" }, "type" : "stupidUsers" }, { "attributes": { "content" : "First Page" }, "id" : "Page 1", "type" : "pages" }, { "attributes": { "content" : "Second Page" }, "id" : "Page 2", "type" : "pages" }, { "attributes": { "content" : "Final page" }, "id" : "Page 3", "type" : "pages" } ] }` testRequest := `{ "data": { "id": "TheOneAndOnlyID", "type": "books", "attributes": {}, "relationships": { "author": { "data": { "id":"A Magical UserID", "type":"users" } }, "pages": { "data": [ { "id": "Page 1", "type": "pages" }, { "id": "Page 2", "type": "pages" }, { "id": "Page 3", "type": "pages" } ] } } } }` Context("Marshal and Unmarshal data", func() { It("Should be marshalled correctly", func() { marshalResult, err := Marshal(testBook) Expect(err).ToNot(HaveOccurred()) Expect(marshalResult).To(MatchJSON(testResult)) }) It("Should be unmarshalled correctly", func() { result := &Book{} expected := Book{ ID: "TheOneAndOnlyID", AuthorID: "A Magical UserID", PagesIDs: []string{"Page 1", "Page 2", "Page 3"}, } err := Unmarshal([]byte(testRequest), result) Expect(err).ToNot(HaveOccurred()) Expect(*result).To(Equal(expected)) }) }) }) api2go-1.0-RC4/jsonapi/jsonapi_suite_test.go000066400000000000000000000002751317567713700210530ustar00rootroot00000000000000package jsonapi import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "testing" ) func TestJsonapi(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Jsonapi Suite") } api2go-1.0-RC4/jsonapi/marshal.go000066400000000000000000000303011317567713700165600ustar00rootroot00000000000000package jsonapi import ( "encoding/json" "errors" "fmt" "reflect" "strings" ) // RelationshipType specifies the type of a relationship. type RelationshipType int // The available relationship types. // // Note: DefaultRelationship guesses the relationship type based on the // pluralization of the reference name. const ( DefaultRelationship RelationshipType = iota ToOneRelationship ToManyRelationship ) // The MarshalIdentifier interface is necessary to give an element a unique ID. // // Note: The implementation of this interface is mandatory. type MarshalIdentifier interface { GetID() string } // ReferenceID contains all necessary information in order to reference another // struct in JSON API. type ReferenceID struct { ID string Type string Name string Relationship RelationshipType } // A Reference information about possible references of a struct. // // Note: If IsNotLoaded is set to true, the `data` field will be omitted and only // the `links` object will be generated. You should do this if there are some // references, but you do not want to load them. Otherwise, if IsNotLoaded is // false and GetReferencedIDs() returns no IDs for this reference name, an // empty `data` field will be added which means that there are no references. type Reference struct { Type string Name string IsNotLoaded bool Relationship RelationshipType } // The MarshalReferences interface must be implemented if the struct to be // serialized has relationships. type MarshalReferences interface { GetReferences() []Reference } // The MarshalLinkedRelations interface must be implemented if there are // reference ids that should be included in the document. type MarshalLinkedRelations interface { MarshalReferences MarshalIdentifier GetReferencedIDs() []ReferenceID } // The MarshalIncludedRelations interface must be implemented if referenced // structs should be included in the document. type MarshalIncludedRelations interface { MarshalReferences MarshalIdentifier GetReferencedStructs() []MarshalIdentifier } // The MarshalCustomLinks interface can be implemented if the struct should // want any custom links. type MarshalCustomLinks interface { MarshalIdentifier GetCustomLinks(string) Links } // The MarshalCustomRelationshipMeta interface can be implemented if the struct should // want a custom meta in a relationship. type MarshalCustomRelationshipMeta interface { MarshalIdentifier GetCustomMeta(string) map[string]Meta } // A ServerInformation implementor can be passed to MarshalWithURLs to generate // the `self` and `related` urls inside `links`. type ServerInformation interface { GetBaseURL() string GetPrefix() string } // MarshalWithURLs can be used to pass along a ServerInformation implementor. func MarshalWithURLs(data interface{}, information ServerInformation) ([]byte, error) { document, err := MarshalToStruct(data, information) if err != nil { return nil, err } return json.Marshal(document) } // Marshal wraps data in a Document and returns its JSON encoding. // // Data can be a struct, a pointer to a struct or a slice of structs. All structs // must at least implement the `MarshalIdentifier` interface. func Marshal(data interface{}) ([]byte, error) { document, err := MarshalToStruct(data, nil) if err != nil { return nil, err } return json.Marshal(document) } // MarshalToStruct marshals an api2go compatible struct into a jsonapi Document // structure which then can be marshaled to JSON. You only need this method if // you want to extract or extend parts of the document. You should directly use // Marshal to get a []byte with JSON in it. func MarshalToStruct(data interface{}, information ServerInformation) (*Document, error) { if data == nil { return &Document{}, nil } switch reflect.TypeOf(data).Kind() { case reflect.Slice: return marshalSlice(data, information) case reflect.Struct, reflect.Ptr: return marshalStruct(data.(MarshalIdentifier), information) default: return nil, errors.New("Marshal only accepts slice, struct or ptr types") } } func recursivelyEmbedIncludes(input []MarshalIdentifier) []MarshalIdentifier { var referencedStructs []MarshalIdentifier for _, referencedStruct := range input { included, ok := referencedStruct.(MarshalIncludedRelations) if ok { referencedStructs = append(referencedStructs, included.GetReferencedStructs()...) } } if len(referencedStructs) == 0 { return input } childStructs := recursivelyEmbedIncludes(referencedStructs) referencedStructs = append(referencedStructs, childStructs...) referencedStructs = append(input, referencedStructs...) return referencedStructs } func marshalSlice(data interface{}, information ServerInformation) (*Document, error) { result := &Document{} val := reflect.ValueOf(data) dataElements := make([]Data, val.Len()) var referencedStructs []MarshalIdentifier for i := 0; i < val.Len(); i++ { k := val.Index(i).Interface() element, ok := k.(MarshalIdentifier) if !ok { return nil, errors.New("all elements within the slice must implement api2go.MarshalIdentifier") } err := marshalData(element, &dataElements[i], information) if err != nil { return nil, err } included, ok := k.(MarshalIncludedRelations) if ok { referencedStructs = append(referencedStructs, included.GetReferencedStructs()...) } } allReferencedStructs := recursivelyEmbedIncludes(referencedStructs) includedElements, err := filterDuplicates(allReferencedStructs, information) if err != nil { return nil, err } result.Data = &DataContainer{ DataArray: dataElements, } if includedElements != nil && len(includedElements) > 0 { result.Included = includedElements } return result, nil } func filterDuplicates(input []MarshalIdentifier, information ServerInformation) ([]Data, error) { alreadyIncluded := map[string]map[string]bool{} includedElements := []Data{} for _, referencedStruct := range input { structType := getStructType(referencedStruct) if alreadyIncluded[structType] == nil { alreadyIncluded[structType] = make(map[string]bool) } if !alreadyIncluded[structType][referencedStruct.GetID()] { var data Data err := marshalData(referencedStruct, &data, information) if err != nil { return nil, err } includedElements = append(includedElements, data) alreadyIncluded[structType][referencedStruct.GetID()] = true } } return includedElements, nil } func marshalData(element MarshalIdentifier, data *Data, information ServerInformation) error { refValue := reflect.ValueOf(element) if refValue.Kind() == reflect.Ptr && refValue.IsNil() { return errors.New("MarshalIdentifier must not be nil") } attributes, err := json.Marshal(element) if err != nil { return err } data.Attributes = attributes data.ID = element.GetID() data.Type = getStructType(element) if information != nil { if customLinks, ok := element.(MarshalCustomLinks); ok { if data.Links == nil { data.Links = make(Links) } base := getLinkBaseURL(element, information) for k, v := range customLinks.GetCustomLinks(base) { if _, ok := data.Links[k]; !ok { data.Links[k] = v } } } } if references, ok := element.(MarshalLinkedRelations); ok { data.Relationships = getStructRelationships(references, information) } return nil } func isToMany(relationshipType RelationshipType, name string) bool { if relationshipType == DefaultRelationship { return Pluralize(name) == name } return relationshipType == ToManyRelationship } func getMetaForRelation(metaSource MarshalCustomRelationshipMeta, name string, information ServerInformation) map[string]interface{} { meta := make(map[string]interface{}) base := getLinkBaseURL(metaSource, information) if metaMap, ok := metaSource.GetCustomMeta(base)[name]; ok { for k, v := range metaMap { if _, ok := meta[k]; !ok { meta[k] = v } } } return meta } func getStructRelationships(relationer MarshalLinkedRelations, information ServerInformation) map[string]Relationship { referencedIDs := relationer.GetReferencedIDs() sortedResults := map[string][]ReferenceID{} relationships := map[string]Relationship{} for _, referenceID := range referencedIDs { sortedResults[referenceID.Name] = append(sortedResults[referenceID.Name], referenceID) } references := relationer.GetReferences() // helper map to check if all references are included to also include empty ones notIncludedReferences := map[string]Reference{} for _, reference := range references { notIncludedReferences[reference.Name] = reference } for name, referenceIDs := range sortedResults { relationships[name] = Relationship{} // if referenceType is plural, we need to use an array for data, otherwise it's just an object container := RelationshipDataContainer{} if isToMany(referenceIDs[0].Relationship, referenceIDs[0].Name) { // multiple elements in links container.DataArray = []RelationshipData{} for _, referenceID := range referenceIDs { container.DataArray = append(container.DataArray, RelationshipData{ Type: referenceID.Type, ID: referenceID.ID, }) } } else { container.DataObject = &RelationshipData{ Type: referenceIDs[0].Type, ID: referenceIDs[0].ID, } } // set URLs if necessary links := getLinksForServerInformation(relationer, name, information) // get the custom meta for this relationship var meta map[string]interface{} if customMetaSource, ok := relationer.(MarshalCustomRelationshipMeta); ok { meta = getMetaForRelation(customMetaSource, name, information) } relationship := Relationship{ Data: &container, Links: links, Meta: meta, } relationships[name] = relationship // this marks the reference as already included delete(notIncludedReferences, referenceIDs[0].Name) } // check for empty references for name, reference := range notIncludedReferences { container := RelationshipDataContainer{} // Plural empty relationships need an empty array and empty to-one need a null in the json if !reference.IsNotLoaded && isToMany(reference.Relationship, reference.Name) { container.DataArray = []RelationshipData{} } links := getLinksForServerInformation(relationer, name, information) // get the custom meta for this relationship var meta map[string]interface{} if customMetaSource, ok := relationer.(MarshalCustomRelationshipMeta); ok { meta = getMetaForRelation(customMetaSource, name, information) } relationship := Relationship{ Links: links, Meta: meta, } // skip relationship data completely if IsNotLoaded is set if !reference.IsNotLoaded { relationship.Data = &container } relationships[name] = relationship } return relationships } func getLinkBaseURL(element MarshalIdentifier, information ServerInformation) string { prefix := strings.Trim(information.GetBaseURL(), "/") namespace := strings.Trim(information.GetPrefix(), "/") structType := getStructType(element) if namespace != "" { prefix += "/" + namespace } return fmt.Sprintf("%s/%s/%s", prefix, structType, element.GetID()) } func getLinksForServerInformation(relationer MarshalLinkedRelations, name string, information ServerInformation) Links { if information == nil { return nil } links := make(Links) base := getLinkBaseURL(relationer, information) links["self"] = Link{Href: fmt.Sprintf("%s/relationships/%s", base, name)} links["related"] = Link{Href: fmt.Sprintf("%s/%s", base, name)} return links } func marshalStruct(data MarshalIdentifier, information ServerInformation) (*Document, error) { var contentData Data err := marshalData(data, &contentData, information) if err != nil { return nil, err } result := &Document{ Data: &DataContainer{ DataObject: &contentData, }, } included, ok := data.(MarshalIncludedRelations) if ok { included, err := filterDuplicates(recursivelyEmbedIncludes(included.GetReferencedStructs()), information) if err != nil { return nil, err } if len(included) > 0 { result.Included = included } } return result, nil } func getStructType(data interface{}) string { entityName, ok := data.(EntityNamer) if ok { return entityName.GetName() } reflectType := reflect.TypeOf(data) if reflectType.Kind() == reflect.Ptr { return Pluralize(Jsonify(reflectType.Elem().Name())) } return Pluralize(Jsonify(reflectType.Name())) } api2go-1.0-RC4/jsonapi/marshal_composition_test.go000066400000000000000000000025501317567713700222470ustar00rootroot00000000000000package jsonapi import ( "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) type TaggedPost struct { SimplePost Tag string `json:"tag"` } var _ = Describe("Embedded struct types", func() { created, _ := time.Parse(time.RFC3339, "2014-11-10T16:30:48.823Z") post := TaggedPost{ SimplePost{ID: "first", Title: "First Post", Text: "Lipsum", Created: created}, "important", } Context("When marshaling objects with struct composition", func() { It("marshals", func() { i, err := Marshal(post) Expect(err).To(BeNil()) Expect(i).To(MatchJSON(`{ "data": { "type":"taggedPosts", "id":"first", "attributes": { "title":"First Post", "text":"Lipsum", "size":0, "created-date":"2014-11-10T16:30:48.823Z", "updated-date":"0001-01-01T00:00:00Z", "tag":"important" } } }`)) }) }) Context("When unmarshaling objects with struct composition", func() { postJSON := `{ "data": { "type": "taggedPosts", "id": "first", "attributes": { "title": "First Post", "text": "Lipsum", "size": 0, "created-date": "2014-11-10T16:30:48.823Z", "tag": "important" } } }` It("unmarshals", func() { target := TaggedPost{} err := Unmarshal([]byte(postJSON), &target) Expect(err).ToNot(HaveOccurred()) Expect(target).To(Equal(post)) }) }) }) api2go-1.0-RC4/jsonapi/marshal_different_to_many_test.go000066400000000000000000000027071317567713700234040ustar00rootroot00000000000000package jsonapi import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) type ManyParent struct { ID string `json:"-"` Content string `json:"content"` } func (m ManyParent) GetID() string { return m.ID } func (m ManyParent) GetReferences() []Reference { return []Reference{ { Type: "childs", Name: "childs", }, } } func (m ManyParent) GetReferencedIDs() []ReferenceID { return []ReferenceID{ { Type: "childs", Name: "childs", ID: "one", }, { Type: "other-childs", Name: "childs", ID: "two", }, } } var _ = Describe("Marshalling toMany relations with the same name and different types", func() { var toMarshal ManyParent BeforeEach(func() { toMarshal = ManyParent{ ID: "one", Content: "test", } }) It("marshals toMany relationships with different type and same name", func() { result, err := Marshal(toMarshal) Expect(err).ToNot(HaveOccurred()) Expect(result).To(MatchJSON(`{ "data": { "attributes": { "content": "test" }, "id": "one", "relationships": { "childs": { "data": [ { "id": "one", "type": "childs" }, { "id": "two", "type": "other-childs" } ] } }, "type": "manyParents" } }`)) }) }) api2go-1.0-RC4/jsonapi/marshal_enum_test.go000066400000000000000000000050641317567713700206530ustar00rootroot00000000000000package jsonapi import ( "encoding/json" "fmt" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) type PublishStatus int const ( StatusUnpublished PublishStatus = iota StatusPublished ) var publishStatusValues = []string{ StatusUnpublished: "unpublished", StatusPublished: "published", } func (s PublishStatus) String() string { if s < 0 || int(s) >= len(publishStatusValues) { panic("value out of range") } return publishStatusValues[s] } func (s PublishStatus) MarshalText() ([]byte, error) { return []byte(s.String()), nil } func (s *PublishStatus) UnmarshalText(text []byte) error { label := string(text) for key, v := range publishStatusValues { if v == label { *s = PublishStatus(key) return nil } } return fmt.Errorf("invalid value `%s`", label) } func (s *PublishStatus) UnmarshalJSON(data []byte) error { var text string if err := json.Unmarshal(data, &text); err != nil { return err } return s.UnmarshalText([]byte(text)) } type EnumPost struct { ID string `json:"-"` Title string `json:"title"` Status PublishStatus `json:"status"` } func (e EnumPost) GetID() string { return e.ID } func (e *EnumPost) SetID(ID string) error { e.ID = ID return nil } var _ = Describe("Custom enum types", func() { status := StatusPublished statusValue := "published" singleJSON := []byte(`{"data":{"id": "1", "type": "enumPosts", "attributes": {"title":"First Post","status":"published"}}}`) firstPost := EnumPost{ID: "1", Title: "First Post", Status: StatusPublished} Context("When marshaling objects including enumes", func() { singlePost := EnumPost{ ID: "1", Title: "First Post", Status: StatusPublished, } It("marshals JSON", func() { result, err := Marshal(singlePost) Expect(err).ToNot(HaveOccurred()) Expect(result).To(MatchJSON(singleJSON)) }) }) Context("When unmarshaling objects including enums", func() { It("unmarshals status string values to int enum type", func() { var result PublishStatus result.UnmarshalText([]byte(statusValue)) Expect(result).To(Equal(status)) }) It("unmarshals single objects into a struct", func() { // Todo: Hm, what was that test for? I don't remember, maybe delete it, but now it checks empty jsons and // raises an error which is also a good thing var post EnumPost err := Unmarshal([]byte("{}"), &post) Expect(err).To(HaveOccurred()) }) It("unmarshals JSON", func() { var post EnumPost err := Unmarshal(singleJSON, &post) Expect(err).ToNot(HaveOccurred()) Expect(post).To(Equal(firstPost)) }) }) }) api2go-1.0-RC4/jsonapi/marshal_same_type_test.go000066400000000000000000000042401317567713700216700ustar00rootroot00000000000000package jsonapi import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) type Node struct { ID string `json:"-"` Content string `json:"content"` MotherID string `json:"-"` ChildIDs []string `json:"-"` AbandonedChildIDs []string `json:"-"` } func (n *Node) GetID() string { return n.ID } func (n *Node) GetReferences() []Reference { return []Reference{ { Type: "nodes", Name: "mother-node", }, { Type: "nodes", Name: "child-nodes", }, { Type: "nodes", Name: "abandoned-child-nodes", }, } } func (n *Node) GetReferencedIDs() []ReferenceID { result := []ReferenceID{} if n.MotherID != "" { result = append(result, ReferenceID{Type: "nodes", Name: "mother-node", ID: n.MotherID}) } for _, referenceID := range n.ChildIDs { result = append(result, ReferenceID{Type: "nodes", Name: "child-nodes", ID: referenceID}) } for _, referenceID := range n.AbandonedChildIDs { result = append(result, ReferenceID{Type: "nodes", Name: "abandoned-child-nodes", ID: referenceID}) } return result } var _ = Describe("Marshalling with the same reference type", func() { var theNode Node BeforeEach(func() { theNode = Node{ ID: "super", Content: "I am the Super Node", MotherID: "1337", ChildIDs: []string{"666", "42"}, AbandonedChildIDs: []string{"2", "1"}, } }) It("marshals all the relationships of the same type", func() { i, err := Marshal(&theNode) Expect(err).To(BeNil()) Expect(i).To(MatchJSON(`{ "data": { "type": "nodes", "id": "super", "attributes": { "content": "I am the Super Node" }, "relationships": { "abandoned-child-nodes": { "data": [ { "type": "nodes", "id": "2" }, { "type": "nodes", "id": "1" } ] }, "child-nodes": { "data": [ { "type": "nodes", "id": "666" }, { "type": "nodes", "id": "42" } ] }, "mother-node": { "data": { "type": "nodes", "id": "1337" } } } } }`)) }) }) api2go-1.0-RC4/jsonapi/marshal_test.go000066400000000000000000001041521317567713700176250ustar00rootroot00000000000000package jsonapi import ( "database/sql" "time" "gopkg.in/guregu/null.v2/zero" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) var _ = Describe("Marshalling", func() { Context("When marshaling simple objects", func() { var ( firstPost, secondPost SimplePost created time.Time ) BeforeEach(func() { created, _ = time.Parse(time.RFC3339, "2014-11-10T16:30:48.823Z") firstPost = SimplePost{ID: "first", Title: "First Post", Text: "Lipsum", Created: created} secondPost = SimplePost{ID: "second", Title: "Second Post", Text: "Getting more advanced!", Created: created, Updated: created} }) It("marshals single object without relationships", func() { user := User{ID: 100, Name: "Nino", Password: "babymaus"} i, err := Marshal(user) Expect(err).To(BeNil()) Expect(i).To(MatchJSON(`{ "data": { "type": "users", "id": "100", "attributes": { "name": "Nino" } } }`)) }) It("marshals single object without relationships as pointer", func() { user := User{ID: 100, Name: "Nino", Password: "babymaus"} i, err := Marshal(&user) Expect(err).To(BeNil()) Expect(i).To(MatchJSON(`{ "data": { "type": "users", "id": "100", "attributes": { "name": "Nino" } } }`)) }) It("marshals single object", func() { i, err := Marshal(firstPost) Expect(err).To(BeNil()) Expect(i).To(MatchJSON(`{ "data": { "type": "simplePosts", "id": "first", "attributes": { "title": "First Post", "text": "Lipsum", "created-date": "2014-11-10T16:30:48.823Z", "updated-date": "0001-01-01T00:00:00Z", "size": 0 } } }`)) }) It("should prefer fmt.Stringer().String() over string contents", func() { m := Magic{} m.ID = "This should be only internal" v, e := Marshal(m) Expect(e).ToNot(HaveOccurred()) Expect(v).To(MatchJSON(`{ "data": { "type": "magics", "id": "This should be visible", "attributes": {} } }`)) }) It("marshal nil value", func() { res, err := Marshal(nil) Expect(err).ToNot(HaveOccurred()) Expect(res).To(MatchJSON(`{"data": null}`)) }) It("marshals collections object", func() { i, err := Marshal([]SimplePost{firstPost, secondPost}) Expect(err).To(BeNil()) Expect(i).To(MatchJSON(`{ "data": [ { "type": "simplePosts", "id": "first", "attributes": { "title": "First Post", "text": "Lipsum", "size": 0, "created-date": "2014-11-10T16:30:48.823Z", "updated-date": "0001-01-01T00:00:00Z" } }, { "type": "simplePosts", "id": "second", "attributes": { "title": "Second Post", "text": "Getting more advanced!", "size": 0, "created-date": "2014-11-10T16:30:48.823Z", "updated-date": "2014-11-10T16:30:48.823Z" } } ] }`)) }) It("marshals empty collections", func() { i, err := Marshal([]SimplePost{}) Expect(err).To(BeNil()) Expect(i).To(MatchJSON(` { "data": [] }`)) }) It("marshals slices of interface with one struct", func() { i, err := Marshal([]interface{}{firstPost}) Expect(err).ToNot(HaveOccurred()) Expect(i).To(MatchJSON(`{ "data": [ { "type": "simplePosts", "id": "first", "attributes": { "title": "First Post", "text": "Lipsum", "size": 0, "created-date": "2014-11-10T16:30:48.823Z", "updated-date": "0001-01-01T00:00:00Z" } } ] }`)) }) It("marshals slices of interface with structs", func() { i, err := Marshal([]interface{}{firstPost, secondPost, User{ID: 1337, Name: "Nino", Password: "God"}}) Expect(err).ToNot(HaveOccurred()) Expect(i).To(MatchJSON(`{ "data": [ { "type": "simplePosts", "id": "first", "attributes": { "title": "First Post", "text": "Lipsum", "size": 0, "created-date": "2014-11-10T16:30:48.823Z", "updated-date": "0001-01-01T00:00:00Z" } }, { "type": "simplePosts", "id": "second", "attributes": { "title": "Second Post", "text": "Getting more advanced!", "size": 0, "created-date": "2014-11-10T16:30:48.823Z", "updated-date": "2014-11-10T16:30:48.823Z" } }, { "type": "users", "id": "1337", "attributes": { "name": "Nino" } } ] }`)) }) It("returns an error when passing an empty string", func() { _, err := Marshal("") Expect(err).To(HaveOccurred()) }) It("MarshalWithURLs catches MarshalToStruct error", func() { _, err := MarshalWithURLs("blubb", CompleteServerInformation{}) Expect(err).To(HaveOccurred()) }) It("returns an error if not every element implements MarshalIdentifier", func() { _, err := Marshal([]interface{}{Comment{ID: 1, Text: "Blubb"}, "invalid"}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("all elements within the slice must implement api2go.MarshalIdentifier")) }) It("return an error if MarshalIdentifier was nil slice", func() { var comment *Comment _, err := Marshal([]interface{}{comment}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("MarshalIdentifier must not be nil")) }) It("return an error if MarshalIdentifier struct was nil", func() { var comment *Comment _, err := Marshal(comment) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("MarshalIdentifier must not be nil")) }) }) Context("When marshaling objects with custom links", func() { It("contains the custom links in the marshaled data", func() { post := CustomLinksPost{} i, err := MarshalWithURLs(post, CompleteServerInformation{}) Expect(err).To(BeNil()) Expect(i).To(MatchJSON(`{ "data": { "type": "posts", "id": "someID", "attributes": {}, "links": { "someLink": "http://my.domain/v1/posts/someID/someLink", "otherLink": { "href": "http://my.domain/v1/posts/someID/otherLink", "meta": {"method": "GET"} } } } }`)) }) }) Context("When marshaling objects with custom meta", func() { It("contains the custom meta in the marshaled data", func() { post := CustomMetaPost{} i, err := MarshalWithURLs(post, CompleteServerInformation{}) Expect(err).To(BeNil()) Expect(i).To(MatchJSON(`{ "data": { "type": "posts", "id": "someID", "attributes": {}, "relationships": { "author": { "links": { "self": "http://my.domain/v1/posts/someID/relationships/author", "related": "http://my.domain/v1/posts/someID/author" }, "meta": { "someMetaKey": "someMetaValue", "someOtherMetaKey": "someOtherMetaValue" } } } } }`)) }) }) Context("When marshaling compound objects", func() { It("marshals nested objects", func() { comment1 := Comment{ID: 1, Text: "First!", SubCommentsEmpty: true} comment2 := Comment{ID: 2, Text: "Second!", SubCommentsEmpty: true} author := User{ID: 1, Name: "Test Author"} post1 := Post{ID: 1, Title: "Foobar", Comments: []Comment{comment1, comment2}, Author: &author} post2 := Post{ID: 2, Title: "Foobarbarbar", Comments: []Comment{comment1, comment2}, Author: &author} posts := []Post{post1, post2} i, err := MarshalWithURLs(posts, CompleteServerInformation{}) Expect(err).To(BeNil()) expected := `{ "data": [ { "type": "posts", "id": "1", "attributes": { "title": "Foobar" }, "relationships": { "author": { "links": { "self": "http://my.domain/v1/posts/1/relationships/author", "related": "http://my.domain/v1/posts/1/author" }, "data": { "type": "users", "id": "1" } }, "comments": { "links": { "self": "http://my.domain/v1/posts/1/relationships/comments", "related": "http://my.domain/v1/posts/1/comments" }, "data": [ { "type": "comments", "id": "1" }, { "type": "comments", "id": "2" } ] } } }, { "type": "posts", "id": "2", "attributes": { "title": "Foobarbarbar" }, "relationships": { "author": { "links": { "self": "http://my.domain/v1/posts/2/relationships/author", "related": "http://my.domain/v1/posts/2/author" }, "data": { "type": "users", "id": "1" } }, "comments": { "links": { "self": "http://my.domain/v1/posts/2/relationships/comments", "related": "http://my.domain/v1/posts/2/comments" }, "data": [ { "type": "comments", "id": "1" }, { "type": "comments", "id": "2" } ] } } } ], "included": [ { "type": "users", "id": "1", "attributes": { "name": "Test Author" } }, { "type": "comments", "id": "1", "attributes": { "text": "First!" }, "relationships": { "comments": { "links": { "related": "http://my.domain/v1/comments/1/comments", "self": "http://my.domain/v1/comments/1/relationships/comments" } } } }, { "type": "comments", "id": "2", "attributes": { "text": "Second!" }, "relationships": { "comments": { "links": { "related": "http://my.domain/v1/comments/2/comments", "self": "http://my.domain/v1/comments/2/relationships/comments" } } } } ] }` Expect(i).To(MatchJSON(expected)) }) It("recursively marshals includes for slices", func() { comment1SubComment1 := Comment{ID: 3, Text: "No you are wrong!", SubCommentsEmpty: true} comment1SubComment2 := Comment{ID: 4, Text: "Nah, he's right!", SubCommentsEmpty: true} comment1 := Comment{ID: 1, Text: "First!", SubComments: []Comment{comment1SubComment1, comment1SubComment2}} comment2 := Comment{ID: 2, Text: "Second!", SubCommentsEmpty: true} author := User{ID: 1, Name: "Test Author"} post1 := Post{ID: 1, Title: "Foobar", Comments: []Comment{comment1, comment2}, Author: &author} post2 := Post{ID: 2, Title: "Foobarbarbar", Comments: []Comment{comment1, comment2}, Author: &author} posts := []Post{post1, post2} i, err := MarshalWithURLs(posts, CompleteServerInformation{}) Expect(err).To(BeNil()) Expect(i).To(MatchJSON(` { "data": [ { "type": "posts", "id": "1", "attributes": { "title": "Foobar" }, "relationships": { "author": { "links": { "related": "http://my.domain/v1/posts/1/author", "self": "http://my.domain/v1/posts/1/relationships/author" }, "data": { "type": "users", "id": "1" } }, "comments": { "links": { "related": "http://my.domain/v1/posts/1/comments", "self": "http://my.domain/v1/posts/1/relationships/comments" }, "data": [ { "type": "comments", "id": "1" }, { "type": "comments", "id": "2" } ] } } }, { "type": "posts", "id": "2", "attributes": { "title": "Foobarbarbar" }, "relationships": { "author": { "links": { "related": "http://my.domain/v1/posts/2/author", "self": "http://my.domain/v1/posts/2/relationships/author" }, "data": { "type": "users", "id": "1" } }, "comments": { "links": { "related": "http://my.domain/v1/posts/2/comments", "self": "http://my.domain/v1/posts/2/relationships/comments" }, "data": [ { "type": "comments", "id": "1" }, { "type": "comments", "id": "2" } ] } } } ], "included": [ { "type": "users", "id": "1", "attributes": { "name": "Test Author" } }, { "type": "comments", "id": "1", "attributes": { "text": "First!" }, "relationships": { "comments": { "links": { "related": "http://my.domain/v1/comments/1/comments", "self": "http://my.domain/v1/comments/1/relationships/comments" }, "data": [ { "type": "comments", "id": "3" }, { "type": "comments", "id": "4" } ] } } }, { "type": "comments", "id": "2", "attributes": { "text": "Second!" }, "relationships": { "comments": { "links": { "related": "http://my.domain/v1/comments/2/comments", "self": "http://my.domain/v1/comments/2/relationships/comments" } } } }, { "type": "comments", "id": "3", "attributes": { "text": "No you are wrong!" }, "relationships": { "comments": { "links": { "related": "http://my.domain/v1/comments/3/comments", "self": "http://my.domain/v1/comments/3/relationships/comments" } } } }, { "type": "comments", "id": "4", "attributes": { "text": "Nah, he's right!" }, "relationships": { "comments": { "links": { "related": "http://my.domain/v1/comments/4/comments", "self": "http://my.domain/v1/comments/4/relationships/comments" } } } } ] } `)) }) It("recursively marshals includes for structs", func() { comment1SubComment1 := Comment{ID: 3, Text: "No you are wrong!", SubCommentsEmpty: true} comment1SubComment2 := Comment{ID: 4, Text: "Nah, he's right!", SubCommentsEmpty: true} comment1 := Comment{ID: 1, Text: "First!", SubComments: []Comment{comment1SubComment1, comment1SubComment2}} comment2 := Comment{ID: 2, Text: "Second!", SubCommentsEmpty: true} author := User{ID: 1, Name: "Test Author"} post1 := Post{ID: 1, Title: "Foobar", Comments: []Comment{comment1, comment2}, Author: &author} i, err := MarshalWithURLs(post1, CompleteServerInformation{}) Expect(err).To(BeNil()) Expect(i).To(MatchJSON(` { "data": { "type": "posts", "id": "1", "attributes": { "title": "Foobar" }, "relationships": { "author": { "links": { "related": "http://my.domain/v1/posts/1/author", "self": "http://my.domain/v1/posts/1/relationships/author" }, "data": { "type": "users", "id": "1" } }, "comments": { "links": { "related": "http://my.domain/v1/posts/1/comments", "self": "http://my.domain/v1/posts/1/relationships/comments" }, "data": [ { "type": "comments", "id": "1" }, { "type": "comments", "id": "2" } ] } } }, "included": [ { "type": "users", "id": "1", "attributes": { "name": "Test Author" } }, { "type": "comments", "id": "1", "attributes": { "text": "First!" }, "relationships": { "comments": { "links": { "related": "http://my.domain/v1/comments/1/comments", "self": "http://my.domain/v1/comments/1/relationships/comments" }, "data": [ { "type": "comments", "id": "3" }, { "type": "comments", "id": "4" } ] } } }, { "type": "comments", "id": "2", "attributes": { "text": "Second!" }, "relationships": { "comments": { "links": { "related": "http://my.domain/v1/comments/2/comments", "self": "http://my.domain/v1/comments/2/relationships/comments" } } } }, { "type": "comments", "id": "3", "attributes": { "text": "No you are wrong!" }, "relationships": { "comments": { "links": { "related": "http://my.domain/v1/comments/3/comments", "self": "http://my.domain/v1/comments/3/relationships/comments" } } } }, { "type": "comments", "id": "4", "attributes": { "text": "Nah, he's right!" }, "relationships": { "comments": { "links": { "related": "http://my.domain/v1/comments/4/comments", "self": "http://my.domain/v1/comments/4/relationships/comments" } } } } ] } `)) }) It("adds IDs", func() { post := Post{ID: 1, Comments: []Comment{}, CommentsIDs: []int{1}} i, err := MarshalWithURLs(post, CompleteServerInformation{}) expected := `{ "data": { "type": "posts", "id": "1", "attributes": { "title": "" }, "relationships": { "author": { "links": { "self": "http://my.domain/v1/posts/1/relationships/author", "related": "http://my.domain/v1/posts/1/author" }, "data": null }, "comments": { "links": { "self": "http://my.domain/v1/posts/1/relationships/comments", "related": "http://my.domain/v1/posts/1/comments" }, "data": [ { "type": "comments", "id": "1" } ] } } } }` Expect(err).To(BeNil()) Expect(i).To(MatchJSON(expected)) }) It("prefers nested structs when given both, structs and IDs", func() { comment := Comment{ID: 1, SubCommentsEmpty: true} author := User{ID: 1, Name: "Tester"} post := Post{ID: 1, Comments: []Comment{comment}, CommentsIDs: []int{2}, Author: &author, AuthorID: sql.NullInt64{Int64: 1337}} i, err := Marshal(post) Expect(err).To(BeNil()) Expect(i).To(MatchJSON(` { "data": { "type": "posts", "id": "1", "attributes": { "title": "" }, "relationships": { "author": { "data": { "type": "users", "id": "1" } }, "comments": { "data": [ { "type": "comments", "id": "1" } ] } } }, "included": [ { "type": "users", "id": "1", "attributes": { "name": "Tester" } }, { "type": "comments", "id": "1", "attributes": { "text": "" }, "relationships": { "comments": {} } } ] }`)) }) It("uses ID field if MarshalLinkedRelations is implemented", func() { anotherPost := AnotherPost{ID: 1, AuthorID: 1} i, err := Marshal(anotherPost) Expect(err).To(BeNil()) Expect(i).To(MatchJSON(`{ "data": { "type": "anotherPosts", "id": "1", "attributes": {}, "relationships": { "author": { "data": { "type": "users", "id": "1" } } } } }`)) }) }) Context("when marshalling with relations that were not loaded", func() { It("skips data field for not loaded relations", func() { post := Post{ID: 123, Title: "Test", CommentsEmpty: true, AuthorEmpty: true} // this only makes sense with MarshalWithURLs. Otherwise, the jsonapi spec would be // violated, because you at least need a data, links, or meta field i, err := MarshalWithURLs(post, CompleteServerInformation{}) Expect(err).ToNot(HaveOccurred()) Expect(i).To(MatchJSON(`{ "data": { "type": "posts", "id": "123", "attributes": { "title": "Test" }, "relationships": { "author": { "links": { "self": "http://my.domain/v1/posts/123/relationships/author", "related": "http://my.domain/v1/posts/123/author" } }, "comments": { "links": { "self": "http://my.domain/v1/posts/123/relationships/comments", "related": "http://my.domain/v1/posts/123/comments" } } } } }`)) }) It("skips data field for not loaded author relation", func() { post := Post{ID: 123, Title: "Test", AuthorEmpty: true} // this only makes sense with MarshalWithURLs. Otherwise, the jsonapi spec would be // violated, because you at least need a data, links, or meta field i, err := MarshalWithURLs(post, CompleteServerInformation{}) Expect(err).ToNot(HaveOccurred()) Expect(i).To(MatchJSON(`{ "data": { "type": "posts", "id": "123", "attributes": { "title": "Test" }, "relationships": { "author": { "links": { "self": "http://my.domain/v1/posts/123/relationships/author", "related": "http://my.domain/v1/posts/123/author" } }, "comments": { "links": { "self": "http://my.domain/v1/posts/123/relationships/comments", "related": "http://my.domain/v1/posts/123/comments" }, "data": [] } } } }`)) }) It("skips data field for not loaded comments", func() { post := Post{ID: 123, Title: "Test", CommentsEmpty: true} // this only makes sense with MarshalWithURLs. Otherwise, the jsonapi spec would be // violated, because you at least need a data, links, or meta field i, err := MarshalWithURLs(post, CompleteServerInformation{}) Expect(err).ToNot(HaveOccurred()) Expect(i).To(MatchJSON(`{ "data": { "type": "posts", "id": "123", "attributes": { "title": "Test" }, "relationships": { "author": { "links": { "self": "http://my.domain/v1/posts/123/relationships/author", "related": "http://my.domain/v1/posts/123/author" }, "data": null }, "comments": { "links": { "self": "http://my.domain/v1/posts/123/relationships/comments", "related": "http://my.domain/v1/posts/123/comments" } } } } }`)) }) }) Context("when marshalling zero value types", func() { theFloat := zero.NewFloat(2.3, true) post := ZeroPost{ID: "1", Title: "test", Value: theFloat} pointerPost := ZeroPostPointer{ID: "1", Title: "test", Value: &theFloat} It("correctly unmarshals driver values", func() { marshalled, err := Marshal(post) Expect(err).To(BeNil()) Expect(marshalled).To(MatchJSON(`{ "data": { "type": "zeroPosts", "id": "1", "attributes": { "title": "test", "value": 2.3 } } }`)) }) It("correctly unmarshals driver values with pointer", func() { marshalled, err := Marshal(pointerPost) Expect(err).To(BeNil()) Expect(marshalled).To(MatchJSON(`{ "data": { "type": "zeroPostPointers", "id": "1", "attributes": { "title": "test", "value": 2.3 } } }`)) }) }) Context("When marshalling objects linking to other instances of the same type", func() { question1 := Question{ID: "1", Text: "Does this test work?"} question1Duplicate := Question{ID: "1", Text: "Does this test work?"} question2 := Question{ID: "2", Text: "Will it ever work?", InspiringQuestionID: sql.NullString{String: "1", Valid: true}, InspiringQuestion: &question1} question3 := Question{ID: "3", Text: "It works now", InspiringQuestionID: sql.NullString{String: "1", Valid: true}, InspiringQuestion: &question1Duplicate} It("Correctly marshalls question2 and sets question1 into included", func() { marshalled, err := Marshal(question2) Expect(err).To(BeNil()) Expect(marshalled).To(MatchJSON(`{ "data": { "type": "questions", "id": "2", "attributes": { "text": "Will it ever work?" }, "relationships": { "inspiringQuestion": { "data": { "type": "questions", "id": "1" } } } }, "included": [ { "type": "questions", "id": "1", "attributes": { "text": "Does this test work?" }, "relationships": { "inspiringQuestion": { "data": null } } } ] }`)) }) It("Does not marshall same dependencies multiple times for slice", func() { marshalled, err := Marshal([]Question{question3, question2}) Expect(err).To(BeNil()) Expect(marshalled).To(MatchJSON(`{ "data": [ { "type": "questions", "id": "3", "attributes": { "text": "It works now" }, "relationships": { "inspiringQuestion": { "data": { "type": "questions", "id": "1" } } } }, { "type": "questions", "id": "2", "attributes": { "text": "Will it ever work?" }, "relationships": { "inspiringQuestion": { "data": { "type": "questions", "id": "1" } } } } ], "included": [ { "type": "questions", "id": "1", "attributes": { "text": "Does this test work?" }, "relationships": { "inspiringQuestion": { "data": null } } } ] }`)) }) It("Does not marshall same dependencies multiple times for single struct", func() { sharedDependency := DeepDedendencies{ID: "4"} marshalled, err := Marshal(DeepDedendencies{ ID: "1", Relationships: []DeepDedendencies{ { ID: "2", Relationships: []DeepDedendencies{sharedDependency}, }, { ID: "3", Relationships: []DeepDedendencies{sharedDependency}, }, }, }) Expect(err).To(BeNil()) Expect(marshalled).To(MatchJSON(`{ "data": { "type": "deep", "id": "1", "attributes": {}, "relationships": { "deps": { "data": [ { "type": "deep", "id": "2" }, { "type": "deep", "id": "3" } ] } } }, "included": [ { "type": "deep", "id": "2", "attributes": {}, "relationships": { "deps": { "data": [ { "type": "deep", "id": "4" } ] } } }, { "type": "deep", "id": "4", "attributes": {}, "relationships": { "deps": { "data": [] } } }, { "type": "deep", "id": "3", "attributes": {}, "relationships": { "deps": { "data": [ { "type": "deep", "id": "4" } ] } } } ] }`)) }) }) Context("Slice fields", func() { It("Marshalls the slice field correctly", func() { marshalled, err := Marshal(Identity{1234, []string{"user_global"}}) Expect(err).To(BeNil()) Expect(marshalled).To(MatchJSON(`{ "data": { "type": "identities", "id": "1234", "attributes": { "scopes": [ "user_global" ] } } }`)) }) It("Marshalls correctly without an ID field", func() { marshalled, err := Marshal(Unicorn{1234, []string{"user_global"}}) Expect(err).To(BeNil()) Expect(marshalled).To(MatchJSON(`{ "data": { "type": "unicorns", "id": "magicalUnicorn", "attributes": { "unicorn_id": 1234, "scopes": [ "user_global" ] } } }`)) }) }) Context("Test getStructTypes method", func() { comment := Comment{ID: 100, Text: "some text"} It("should work with normal value", func() { result := getStructType(comment) Expect(result).To(Equal("comments")) }) It("should work with pointer to value", func() { result := getStructType(&comment) Expect(result).To(Equal("comments")) }) It("checks for EntityNamer interface", func() { result := getStructType(RenamedComment{"something"}) Expect(result).To(Equal("renamed-comments")) }) }) Context("test getStructLinks", func() { var ( post Post comment Comment author User ) BeforeEach(func() { comment = Comment{ID: 1} author = User{ID: 1, Name: "Tester"} post = Post{ID: 1, Comments: []Comment{comment}, Author: &author} }) It("Generates to-one relationships correctly", func() { links := getStructRelationships(post, nil) Expect(links["author"]).To(Equal(Relationship{ Data: &RelationshipDataContainer{ DataObject: &RelationshipData{ ID: "1", Type: "users", }, }, })) }) It("Generates to-many relationships correctly", func() { links := getStructRelationships(post, nil) Expect(links["comments"]).To(Equal(Relationship{ Data: &RelationshipDataContainer{ DataArray: []RelationshipData{ { ID: "1", Type: "comments", }, }, }, })) }) It("Generates self/related URLs with baseURL and prefix correctly", func() { links := getStructRelationships(post, CompleteServerInformation{}) Expect(links["author"]).To(Equal(Relationship{ Data: &RelationshipDataContainer{ DataObject: &RelationshipData{ ID: "1", Type: "users", }, }, Links: Links{ "self": Link{Href: "http://my.domain/v1/posts/1/relationships/author"}, "related": Link{Href: "http://my.domain/v1/posts/1/author"}, }, })) }) It("Generates self/related URLs with baseURL correctly", func() { links := getStructRelationships(post, BaseURLServerInformation{}) Expect(links["author"]).To(Equal(Relationship{ Data: &RelationshipDataContainer{ DataObject: &RelationshipData{ ID: "1", Type: "users", }, }, Links: Links{ "self": Link{Href: "http://my.domain/posts/1/relationships/author"}, "related": Link{Href: "http://my.domain/posts/1/author"}, }, })) }) It("Generates self/related URLs with prefix correctly", func() { links := getStructRelationships(post, PrefixServerInformation{}) Expect(links["author"]).To(Equal(Relationship{ Data: &RelationshipDataContainer{ DataObject: &RelationshipData{ ID: "1", Type: "users", }, }, Links: Links{ "self": Link{Href: "/v1/posts/1/relationships/author"}, "related": Link{Href: "/v1/posts/1/author"}, }, })) }) }) Context("test filterDuplicates", func() { input := []MarshalIdentifier{ User{ID: 314, Name: "User314"}, Comment{ID: 314}, Comment{ID: 1}, User{ID: 1, Name: "User1"}, User{ID: 2, Name: "User2"}, Comment{ID: 1}, Comment{ID: 314}, User{ID: 2, Name: "User2Kopie"}, } // this is the wrong format but hey, it's just used to count the length :P expected := []map[string]interface{}{ {"id": 314, "name": "User314", "type": "users"}, {"text": "", "id": 314, "type": "comments"}, {"text": "", "id": 1, "type": "comments"}, {"name": "User1", "id": 1, "type": "users"}, {"name": "User2", "id": 2, "type": "users"}, } It("should work with default marshalData", func() { actual, err := filterDuplicates(input, nil) Expect(err).ToNot(HaveOccurred()) Expect(len(actual)).To(Equal(len(expected))) }) }) // In order to use the SQL Null-Types the Marshal/Unmarshal interfaces for these types must be implemented. // The library "gopkg.in/guregu/null.v2/zero" can be used for that. Context("SQL Null-Types", func() { var nullPost SQLNullPost It("correctly marshalls String, Int64, Float64, Bool and Time", func() { nullPost = SQLNullPost{ ID: "theID", Title: zero.StringFrom("Test"), Likes: zero.IntFrom(666), Rating: zero.FloatFrom(66.66), IsCool: zero.BoolFrom(true), } result, err := Marshal(nullPost) Expect(err).ToNot(HaveOccurred()) Expect(result).To(MatchJSON(`{ "data": { "id": "theID", "type": "sqlNullPosts", "attributes": { "title": "Test", "likes": 666, "rating": 66.66, "isCool": true, "today": "0001-01-01T00:00:00Z" } } }`)) }) It("correctly marshalls Null String, Int64, Float64, Bool and Time", func() { nullPost = SQLNullPost{ ID: "theID", Title: zero.StringFromPtr(nil), Likes: zero.IntFromPtr(nil), Rating: zero.FloatFromPtr(nil), IsCool: zero.BoolFromPtr(nil), Today: zero.TimeFromPtr(nil), } result, err := Marshal(nullPost) Expect(err).ToNot(HaveOccurred()) Expect(result).To(MatchJSON(`{ "data": { "id": "theID", "type": "sqlNullPosts", "attributes": { "title": "", "likes": 0, "rating": 0, "isCool": false, "today": "0001-01-01T00:00:00Z" } } }`)) }) }) Context("when overriding default relationship type", func() { It("defaults to relationship name pluralization - singular", func() { article := Article{Name: "author", Relationship: DefaultRelationship} result, err := Marshal(article) Expect(err).ToNot(HaveOccurred()) Expect(result).To(MatchJSON(`{ "data": { "type": "articles", "id": "id", "attributes": {}, "relationships": { "author": { "data": null } } } }`)) }) It("defaults to relationship name pluralization - plural", func() { article := Article{Name: "authors", Relationship: DefaultRelationship} result, err := Marshal(article) Expect(err).ToNot(HaveOccurred()) Expect(result).To(MatchJSON(`{ "data": { "type": "articles", "id": "id", "attributes": {}, "relationships": { "authors": { "data": [] } } } }`)) }) It("can make a to-many relationship", func() { article := Article{ IDs: []string{"1", "2"}, Name: "author", Type: "users", Relationship: ToManyRelationship, } result, err := Marshal(article) Expect(err).ToNot(HaveOccurred()) Expect(result).To(MatchJSON(`{ "data": { "type": "articles", "id": "id", "attributes": {}, "relationships": { "author": { "data": [ {"type": "users", "id": "1"}, {"type": "users", "id": "2"} ] } } } }`)) }) It("can make a to-one relationship", func() { article := Article{ IDs: []string{"1"}, Name: "authors", Type: "users", Relationship: ToOneRelationship, } result, err := Marshal(article) Expect(err).ToNot(HaveOccurred()) Expect(result).To(MatchJSON(`{ "data": { "type": "articles", "id": "id", "attributes": {}, "relationships": { "authors": { "data": {"type": "users", "id": "1"} } } } }`)) }) }) }) api2go-1.0-RC4/jsonapi/unmarshal.go000066400000000000000000000141641317567713700171340ustar00rootroot00000000000000package jsonapi import ( "encoding/json" "errors" "fmt" "reflect" ) // The UnmarshalIdentifier interface must be implemented to set the ID during // unmarshalling. type UnmarshalIdentifier interface { SetID(string) error } // The UnmarshalToOneRelations interface must be implemented to unmarshal // to-one relations. type UnmarshalToOneRelations interface { SetToOneReferenceID(name, ID string) error } // The UnmarshalToManyRelations interface must be implemented to unmarshal // to-many relations. type UnmarshalToManyRelations interface { SetToManyReferenceIDs(name string, IDs []string) error } // The EditToManyRelations interface can be optionally implemented to add and // delete to-many relationships on a already unmarshalled struct. These methods // are used by our API for the to-many relationship update routes. // // There are 3 HTTP Methods to edit to-many relations: // // PATCH /v1/posts/1/comments // Content-Type: application/vnd.api+json // Accept: application/vnd.api+json // // { // "data": [ // { "type": "comments", "id": "2" }, // { "type": "comments", "id": "3" } // ] // } // // This replaces all of the comments that belong to post with ID 1 and the // SetToManyReferenceIDs method will be called. // // POST /v1/posts/1/comments // Content-Type: application/vnd.api+json // Accept: application/vnd.api+json // // { // "data": [ // { "type": "comments", "id": "123" } // ] // } // // Adds a new comment to the post with ID 1. // The AddToManyIDs method will be called. // // DELETE /v1/posts/1/comments // Content-Type: application/vnd.api+json // Accept: application/vnd.api+json // // { // "data": [ // { "type": "comments", "id": "12" }, // { "type": "comments", "id": "13" } // ] // } // // Deletes comments that belong to post with ID 1. // The DeleteToManyIDs method will be called. type EditToManyRelations interface { AddToManyIDs(name string, IDs []string) error DeleteToManyIDs(name string, IDs []string) error } // Unmarshal parses a JSON API compatible JSON and populates the target which // must implement the `UnmarshalIdentifier` interface. func Unmarshal(data []byte, target interface{}) error { if target == nil { return errors.New("target must not be nil") } if reflect.TypeOf(target).Kind() != reflect.Ptr { return errors.New("target must be a ptr") } ctx := &Document{} err := json.Unmarshal(data, ctx) if err != nil { return err } if ctx.Data == nil { return errors.New(`Source JSON is empty and has no "attributes" payload object`) } if ctx.Data.DataObject != nil { return setDataIntoTarget(ctx.Data.DataObject, target) } if ctx.Data.DataArray != nil { targetSlice := reflect.TypeOf(target).Elem() if targetSlice.Kind() != reflect.Slice { return fmt.Errorf("Cannot unmarshal array to struct target %s", targetSlice) } targetType := targetSlice.Elem() targetPointer := reflect.ValueOf(target) targetValue := targetPointer.Elem() for _, record := range ctx.Data.DataArray { // check if there already is an entry with the same id in target slice, // otherwise create a new target and append var targetRecord, emptyValue reflect.Value for i := 0; i < targetValue.Len(); i++ { marshalCasted, ok := targetValue.Index(i).Interface().(MarshalIdentifier) if !ok { return errors.New("existing structs must implement interface MarshalIdentifier") } if record.ID == marshalCasted.GetID() { targetRecord = targetValue.Index(i).Addr() break } } if targetRecord == emptyValue || targetRecord.IsNil() { targetRecord = reflect.New(targetType) err := setDataIntoTarget(&record, targetRecord.Interface()) if err != nil { return err } targetValue = reflect.Append(targetValue, targetRecord.Elem()) } else { err := setDataIntoTarget(&record, targetRecord.Interface()) if err != nil { return err } } } targetPointer.Elem().Set(targetValue) } return nil } func setDataIntoTarget(data *Data, target interface{}) error { castedTarget, ok := target.(UnmarshalIdentifier) if !ok { return errors.New("target must implement UnmarshalIdentifier interface") } if data.Type == "" { return errors.New("invalid record, no type was specified") } err := checkType(data.Type, castedTarget) if err != nil { return err } if data.Attributes != nil { err = json.Unmarshal(data.Attributes, castedTarget) if err != nil { return err } } if err := castedTarget.SetID(data.ID); err != nil { return err } return setRelationshipIDs(data.Relationships, castedTarget) } // extracts all found relationships and set's them via SetToOneReferenceID or // SetToManyReferenceIDs func setRelationshipIDs(relationships map[string]Relationship, target UnmarshalIdentifier) error { for name, rel := range relationships { // if Data is nil, it means that we have an empty toOne relationship if rel.Data == nil { castedToOne, ok := target.(UnmarshalToOneRelations) if !ok { return fmt.Errorf("struct %s does not implement UnmarshalToOneRelations", reflect.TypeOf(target)) } castedToOne.SetToOneReferenceID(name, "") break } // valid toOne case if rel.Data.DataObject != nil { castedToOne, ok := target.(UnmarshalToOneRelations) if !ok { return fmt.Errorf("struct %s does not implement UnmarshalToOneRelations", reflect.TypeOf(target)) } err := castedToOne.SetToOneReferenceID(name, rel.Data.DataObject.ID) if err != nil { return err } } // valid toMany case if rel.Data.DataArray != nil { castedToMany, ok := target.(UnmarshalToManyRelations) if !ok { return fmt.Errorf("struct %s does not implement UnmarshalToManyRelations", reflect.TypeOf(target)) } IDs := make([]string, len(rel.Data.DataArray)) for index, relData := range rel.Data.DataArray { IDs[index] = relData.ID } err := castedToMany.SetToManyReferenceIDs(name, IDs) if err != nil { return err } } } return nil } func checkType(incomingType string, target UnmarshalIdentifier) error { actualType := getStructType(target) if incomingType != actualType { return fmt.Errorf("Type %s in JSON does not match target struct type %s", incomingType, actualType) } return nil } api2go-1.0-RC4/jsonapi/unmarshal_test.go000066400000000000000000000430061317567713700201700ustar00rootroot00000000000000package jsonapi import ( "database/sql" "encoding/json" "fmt" "time" "gopkg.in/guregu/null.v2/zero" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) var _ = Describe("Unmarshal", func() { Context("When unmarshaling simple objects", func() { t, _ := time.Parse(time.RFC3339, "2014-11-10T16:30:48.823Z") firstPost := SimplePost{ID: "1", Title: "First Post", Text: "Lipsum", Created: t} secondPost := SimplePost{ID: "2", Title: "Second Post", Text: "Foobar!", Created: t, Updated: t} singlePostJSON := []byte(`{ "data": { "id": "1", "type": "simplePosts", "attributes": { "title": "First Post", "text": "Lipsum", "created-date": "2014-11-10T16:30:48.823Z" } } }`) multiplePostJSON := []byte(`{ "data": [ { "id": "1", "type": "simplePosts", "attributes": { "title": "First Post", "text": "Lipsum", "created-date": "2014-11-10T16:30:48.823Z" } }, { "id": "2", "type": "simplePosts", "attributes": { "title": "Second Post", "text": "Foobar!", "created-date": "2014-11-10T16:30:48.823Z", "updated-date": "2014-11-10T16:30:48.823Z" } } ] }`) It("unmarshals single object into a struct", func() { var post SimplePost err := Unmarshal(singlePostJSON, &post) Expect(err).ToNot(HaveOccurred()) Expect(post).To(Equal(firstPost)) }) It("unmarshals multiple objects into a slice", func() { var posts []SimplePost err := Unmarshal(multiplePostJSON, &posts) Expect(err).To(BeNil()) Expect(posts).To(Equal([]SimplePost{firstPost, secondPost})) }) It("unmarshals array attributes into array structs", func() { expected := Image{ ID: "one", Ports: []ImagePort{ { Protocol: "something", Number: 666, }, }, } var actual Image err := Unmarshal([]byte(`{ "data": { "type": "images", "id": "one", "attributes": { "image-ports": [ { "protocol": "something", "number": 666 } ] } } }`), &actual) Expect(err).To(BeNil()) Expect(actual).To(Equal(expected)) }) It("errors on existing record that does not implement MarshalIdentifier", func() { type invalid struct{} invalids := []interface{}{invalid{}} err := Unmarshal(multiplePostJSON, &invalids) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("existing structs must implement interface MarshalIdentifier")) }) It("errors on invalid ID", func() { post := ErrorIDPost{Error: fmt.Errorf("error")} err := Unmarshal([]byte(`{ "data": { "type": "errorIDPosts", "attributes": { "title": "test" } } }`), &post) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("error")) }) It("errors on invalid param nil", func() { err := Unmarshal(singlePostJSON, nil) Expect(err).Should(HaveOccurred()) }) It("errors on invalid param map", func() { err := Unmarshal(singlePostJSON, []interface{}{}) Expect(err).Should(HaveOccurred()) }) It("errors on invalid pointer", func() { err := Unmarshal(singlePostJSON, &[]interface{}{}) Expect(err).Should(HaveOccurred()) }) It("errors on non-array root", func() { var post SimplePost err := Unmarshal([]byte(`{ "data": 42 }`), &post) Expect(err).Should(HaveOccurred()) }) It("errors on non-documents", func() { var post SimplePost err := Unmarshal([]byte(`{ "data": {42} }`), &post) Expect(err).Should(HaveOccurred()) }) It("it ignores fields that can not be unmarshaled like the nomral json.Unmarshaler", func() { var post SimplePost err := Unmarshal([]byte(`{ "data": { "attributes": { "title": "something", "text": "blubb", "internal": "1337" }, "type": "simplePosts" } }`), &post) Expect(err).ShouldNot(HaveOccurred()) }) It("errors with wrong type, expected int, got a string", func() { var post SimplePost err := Unmarshal([]byte(`{ "data": { "attributes": { "text": "Gopher", "size": "blubb" }, "type": "simplePosts" } }`), &post) Expect(err).To(HaveOccurred()) Expect(err).Should(BeAssignableToTypeOf(&json.UnmarshalTypeError{})) typeError := err.(*json.UnmarshalTypeError) Expect(typeError.Value).To(Equal("string")) }) It("errors with invalid time format", func() { t, err := time.Parse(time.RFC3339, "2014-11-10T16:30:48.823Z") faultyPostMap := []byte(`{ "data": { "attributes": { "title": "` + firstPost.Title + `", "text": "` + firstPost.Text + `!", "created-date": "` + t.Format(time.RFC1123) + `" }, "type": "simplePosts" } }`) var post SimplePost err = Unmarshal(faultyPostMap, &post) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("parsing time")) }) It("empty attributes is OK", func() { json := []byte(`{ "data": [{ "type": "simplePosts" }] }`) var posts []SimplePost err := Unmarshal(json, &posts) Expect(err).ToNot(HaveOccurred()) }) It("errors if target is no pointer", func() { json := []byte(`{ "data": { "type": "simplePosts" } }`) err := Unmarshal(json, SimplePost{}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("target must be a ptr")) }) It("errors if json array cannot be unmarshaled into a struct", func() { json := []byte(`{ "data": [{ "type": "simplePosts", "attributes": { "title": "something" } }] }`) var post SimplePost err := Unmarshal(json, &post) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("Cannot unmarshal array to struct target jsonapi.SimplePost")) }) Context("slice fields", func() { It("unmarshal slice fields with single entry correctly", func() { sliceJSON := []byte(`{ "data": { "id": "1234", "type": "identities", "attributes": { "scopes": [ "user_global" ] } } }`) var identity Identity err := Unmarshal(sliceJSON, &identity) Expect(err).ToNot(HaveOccurred()) Expect(identity.Scopes).To(HaveLen(1)) Expect(identity.Scopes[0]).To(Equal("user_global")) }) It("unmarshal slice fields with multiple entries", func() { input := `{ "data": { "id": "1234", "type": "identities", "attributes": { "scopes": ["test", "1234"] } } }` var identity Identity err := Unmarshal([]byte(input), &identity) Expect(err).ToNot(HaveOccurred()) Expect(identity.Scopes[0]).To(Equal("test")) Expect(identity.Scopes[1]).To(Equal("1234")) }) It("unmarshal empty slice fields from json input", func() { input := `{ "data": { "id": "1234", "type": "identities", "attributes": { "scopes": [] } } }` var identity Identity err := Unmarshal([]byte(input), &identity) Expect(err).ToNot(HaveOccurred()) Expect(identity.Scopes).To(Equal([]string{})) }) It("unmarshals renamed fields", func() { input := `{ "data": { "id": "1", "type": "renamedPostWithEmbeddings", "attributes": { "foo": "field content", "bar-bar": "other content", "another": "foo" } } }` var renamedPost RenamedPostWithEmbedding err := Unmarshal([]byte(input), &renamedPost) Expect(err).ToNot(HaveOccurred()) Expect(renamedPost.Field).To(Equal("field content")) Expect(renamedPost.Other).To(Equal("other content")) }) }) }) Context("when unmarshaling objects with relationships", func() { It("unmarshals to-many relationship IDs", func() { expectedPost := Post{ID: 1, CommentsIDs: []int{1}} postJSON := []byte(`{ "data": { "id": "1", "type": "posts", "attributes": {}, "relationships": { "comments": { "data": [ { "id": "1", "type": "links" }] } } } }`) var post Post err := Unmarshal(postJSON, &post) Expect(err).To(BeNil()) Expect(expectedPost).To(Equal(post)) }) It("unmarshals aliased relationships with array data payload", func() { post := Post{ID: 1, CommentsIDs: []int{1}} postJSON := []byte(`{ "data": [{ "id": "1", "type": "posts", "attributes": {"title": "` + post.Title + `"}, "relationships": { "comments": { "data": [{ "id": "1", "type": "votes" }] } } }] }`) var posts []Post err := Unmarshal(postJSON, &posts) Expect(err).To(BeNil()) Expect(posts).To(Equal([]Post{post})) }) It("unmarshal to-one and to-many relations", func() { expectedPost := Post{ID: 3, Title: "Test", AuthorID: sql.NullInt64{Valid: true, Int64: 1}, Author: nil, CommentsIDs: []int{1, 2}} postJSON := []byte(`{ "data": { "id": "3", "type": "posts", "attributes": { "title": "Test" }, "relationships": { "author": { "data": { "id": "1", "type": "users" } }, "comments": { "data": [ { "id": "1", "type": "comments" }, { "id": "2", "type": "comments" } ] } } } }`) var post Post err := Unmarshal(postJSON, &post) Expect(err).To(BeNil()) Expect(post).To(Equal(expectedPost)) }) It("unmarshals empty relationships", func() { expectedPost := Post{ID: 3, Title: "Test", AuthorID: sql.NullInt64{Valid: false, Int64: 0}, Author: nil, CommentsIDs: []int{}} postJSON := []byte(`{ "data": { "id": "3", "type": "posts", "attributes": { "title": "Test" }, "relationships": { "author": { "data": null }, "comments": { "data": [] } } } }`) post := Post{CommentsIDs: []int{}} err := Unmarshal(postJSON, &post) Expect(err).To(BeNil()) Expect(post).To(Equal(expectedPost)) }) It("errors if target does not implement UnmarshalToOneRelations for empty relationship", func() { postJSON := []byte(`{ "data": { "id": "3", "type": "posts", "attributes": { "title": "Test" }, "relationships": { "author": { "data": null } } } }`) post := NoRelationshipPosts{} err := Unmarshal(postJSON, &post) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("struct *jsonapi.NoRelationshipPosts does not implement UnmarshalToOneRelations")) }) Context("UnmarshalToOneRelations error handling", func() { postJSON := []byte(`{ "data": { "id": "3", "type": "posts", "attributes": { "title": "Test" }, "relationships": { "author": { "data": { "id": "1", "type": "users" } } } } }`) It("errors if target does not implement UnmarshalToOneRelations", func() { post := NoRelationshipPosts{} err := Unmarshal(postJSON, &post) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("struct *jsonapi.NoRelationshipPosts does not implement UnmarshalToOneRelations")) }) It("returns an error if SetToOneReferenceID returned an error", func() { post := ErrorRelationshipPosts{} err := Unmarshal(postJSON, &post) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("this never works")) }) }) Context("UnmarshalToManyRelations error handling", func() { postJSON := []byte(`{ "data": { "id": "3", "type": "posts", "attributes": { "title": "Test" }, "relationships": { "comments": { "data": [{ "id": "1", "type": "comments" }] } } } }`) It("errors if target does not implement UnmarshalToManyRelations", func() { post := NoRelationshipPosts{} err := Unmarshal(postJSON, &post) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("struct *jsonapi.NoRelationshipPosts does not implement UnmarshalToManyRelations")) }) It("returns an error if SetTOManyReferenceIDs returned an error", func() { post := ErrorRelationshipPosts{} err := Unmarshal(postJSON, &post) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("this also never works")) }) }) }) It("check if type field matches target struct", func() { postJSON := []byte(`{ "data": { "id": "1", "type": "totallyWrongType", "attributes": { "title": "Test" } } }`) var post Post err := Unmarshal(postJSON, &post) Expect(err).To(HaveOccurred()) }) Context("when unmarshaling into an existing slice", func() { It("overrides existing entries", func() { post := Post{ID: 1, Title: "Old Title"} postJSON := []byte(`{ "data": [{ "id": "1", "type": "posts", "attributes": { "title": "New Title" } }] }`) posts := []Post{post} err := Unmarshal(postJSON, &posts) Expect(err).To(BeNil()) Expect(posts).To(Equal([]Post{{ID: 1, Title: "New Title"}})) }) }) Context("when unmarshaling with null values", func() { It("adding a new entry", func() { expectedPost := SimplePost{ID: "1", Title: "Nice Title"} postJSON := []byte(`{ "data": { "id": "1", "type": "simplePosts", "attributes": { "title": "Nice Title", "text": null } } }`) var post SimplePost err := Unmarshal(postJSON, &post) Expect(err).To(BeNil()) Expect(post).To(Equal(expectedPost)) }) }) Context("when unmarshaling without id", func() { It("adding a new entry", func() { expectedPost := SimplePost{Title: "Nice Title"} postJSON := []byte(` { "data": { "type": "simplePosts", "attributes": { "title": "Nice Title" } } }`) var post SimplePost err := Unmarshal(postJSON, &post) Expect(err).To(BeNil()) Expect(post).To(Equal(expectedPost)) }) }) Context("when unmarshalling objects with numbers", func() { It("correctly converts number to int64", func() { json := `{ "data": [ { "id": "test", "type": "numberPosts", "attributes": { "title": "Blubb", "number": 1337 } } ] }` var numberPosts []NumberPost err := Unmarshal([]byte(json), &numberPosts) Expect(err).ToNot(HaveOccurred()) Expect(len(numberPosts)).To(Equal(1)) Expect(numberPosts[0].Number).To(Equal(int64(1337))) }) It("correctly converts negative number to int64", func() { json := `{ "data": [ { "id": "test", "type": "numberPosts", "attributes": { "title": "Blubb", "number": -1337 } } ] }` var numberPosts []NumberPost err := Unmarshal([]byte(json), &numberPosts) Expect(err).ToNot(HaveOccurred()) Expect(len(numberPosts)).To(Equal(1)) Expect(numberPosts[0].Number).To(Equal(int64(-1337))) }) It("correctly converts number to uint64", func() { json := `{ "data": [ { "id": "test", "type": "numberPosts", "attributes": { "title": "Blubb", "unsignedNumber": 1337 } } ] }` var numberPosts []NumberPost err := Unmarshal([]byte(json), &numberPosts) Expect(err).ToNot(HaveOccurred()) Expect(len(numberPosts)).To(Equal(1)) Expect(numberPosts[0].UnsignedNumber).To(Equal(uint64(1337))) }) }) Context("SQL Null-Types", func() { var ( nullPost SQLNullPost timeZero time.Time ) BeforeEach(func() { nullPost = SQLNullPost{} timeZero = time.Time{} }) It("correctly unmarshals String, Int64, Float64 and Time", func() { err := Unmarshal([]byte(fmt.Sprintf(`{ "data": { "id": "theID", "type": "sqlNullPosts", "attributes": { "title": "Test", "likes": 666, "rating": 66.66, "isCool": true, "today": "%v" } } }`, timeZero.Format(time.RFC3339))), &nullPost) Expect(err).ToNot(HaveOccurred()) Expect(nullPost).To(Equal(SQLNullPost{ ID: "theID", Title: zero.StringFrom("Test"), Likes: zero.IntFrom(666), Rating: zero.FloatFrom(66.66), IsCool: zero.BoolFrom(true), Today: zero.TimeFrom(timeZero.UTC()), })) }) It("correctly unmarshals Null String, Int64, Float64 and Time", func() { err := Unmarshal([]byte(`{ "data": { "id": "theID", "type": "sqlNullPosts", "attributes": { "title": null, "likes": null, "rating": null, "isCool": null, "today": null } } }`), &nullPost) Expect(err).ToNot(HaveOccurred()) Expect(nullPost).To(Equal(SQLNullPost{ ID: "theID", Title: zero.StringFromPtr(nil), Likes: zero.IntFromPtr(nil), Rating: zero.FloatFromPtr(nil), IsCool: zero.BoolFromPtr(nil), Today: zero.TimeFromPtr(nil), })) }) // No it will not do this because of the implementation in zero library. It("sets existing zero value to invalid when unmarshaling null values", func() { target := SQLNullPost{ ID: "newID", Title: zero.StringFrom("TestTitle"), Likes: zero.IntFrom(11), IsCool: zero.BoolFrom(true), Rating: zero.FloatFrom(4.5), Today: zero.TimeFrom(time.Now().UTC())} err := Unmarshal([]byte(`{ "data": { "id": "newID", "type": "sqlNullPosts", "attributes": { "title": null, "likes": null, "rating": null, "isCool": null, "today": null } } }`), &target) Expect(err).ToNot(HaveOccurred()) Expect(target.Title.Valid).To(Equal(false)) Expect(target.Likes.Valid).To(Equal(false)) Expect(target.Rating.Valid).To(Equal(false)) Expect(target.IsCool.Valid).To(Equal(false)) Expect(target.Today.Valid).To(Equal(false)) }) }) }) api2go-1.0-RC4/request.go000066400000000000000000000004271317567713700151640ustar00rootroot00000000000000package api2go import "net/http" // Request contains additional information for FindOne and Find Requests type Request struct { PlainRequest *http.Request QueryParams map[string][]string Pagination map[string]string Header http.Header Context APIContexter } api2go-1.0-RC4/resolver.go000066400000000000000000000020111317567713700153240ustar00rootroot00000000000000package api2go import "net/http" type callbackResolver struct { callback func(r http.Request) string r http.Request } // NewCallbackResolver handles each resolve via // your provided callback func func NewCallbackResolver(callback func(http.Request) string) URLResolver { return &callbackResolver{callback: callback} } // GetBaseURL calls the callback given in the constructor method // to implement `URLResolver` func (c callbackResolver) GetBaseURL() string { return c.callback(c.r) } // SetRequest to implement `RequestAwareURLResolver` func (c *callbackResolver) SetRequest(r http.Request) { c.r = r } // staticResolver is only used // for backwards compatible reasons // and might be removed in the future type staticResolver struct { baseURL string } func (s staticResolver) GetBaseURL() string { return s.baseURL } // NewStaticResolver returns a simple resolver that // will always answer with the same url func NewStaticResolver(baseURL string) URLResolver { return &staticResolver{baseURL: baseURL} } api2go-1.0-RC4/resolver_test.go000066400000000000000000000015331317567713700163730ustar00rootroot00000000000000package api2go import ( "net/http" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) var _ = Describe("Resolver test", func() { Context("basic function of callback resolver", func() { It("works", func() { callback := func(r http.Request) string { if r.Header.Get("lol") != "" { return "funny" } return "unfunny" } resolver := NewCallbackResolver(callback) Expect(resolver.GetBaseURL()).To(Equal("unfunny")) req, err := http.NewRequest("GET", "/v1/posts", nil) req.Header.Set("lol", "lol") Expect(err).To(BeNil()) requestResolver, ok := resolver.(RequestAwareURLResolver) Expect(ok).To(Equal(true), "does not implement interface") Expect(requestResolver.GetBaseURL()).To(Equal("unfunny")) requestResolver.SetRequest(*req) Expect(requestResolver.GetBaseURL()).To(Equal("funny")) }) }) }) api2go-1.0-RC4/response.go000066400000000000000000000032441317567713700153320ustar00rootroot00000000000000package api2go import ( "fmt" "net/http" "net/url" "github.com/manyminds/api2go/jsonapi" ) // The Response struct implements api2go.Responder and can be used as a default // implementation for your responses // you can fill the field `Meta` with all the metadata your application needs // like license, tokens, etc type Response struct { Res interface{} Code int Meta map[string]interface{} Pagination Pagination } // Metadata returns additional meta data func (r Response) Metadata() map[string]interface{} { return r.Meta } // Result returns the actual payload func (r Response) Result() interface{} { return r.Res } // StatusCode sets the return status code func (r Response) StatusCode() int { return r.Code } func buildLink(base string, r *http.Request, pagination map[string]string) jsonapi.Link { params := r.URL.Query() for k, v := range pagination { qk := fmt.Sprintf("page[%s]", k) params.Set(qk, v) } if len(params) == 0 { return jsonapi.Link{Href: base} } query, _ := url.QueryUnescape(params.Encode()) return jsonapi.Link{Href: fmt.Sprintf("%s?%s", base, query)} } // Links returns a jsonapi.Links object to include in the top-level response func (r Response) Links(req *http.Request, baseURL string) (ret jsonapi.Links) { ret = make(jsonapi.Links) if r.Pagination.Next != nil { ret["next"] = buildLink(baseURL, req, r.Pagination.Next) } if r.Pagination.Prev != nil { ret["prev"] = buildLink(baseURL, req, r.Pagination.Prev) } if r.Pagination.First != nil { ret["first"] = buildLink(baseURL, req, r.Pagination.First) } if r.Pagination.Last != nil { ret["last"] = buildLink(baseURL, req, r.Pagination.Last) } return } api2go-1.0-RC4/routing/000077500000000000000000000000001317567713700146315ustar00rootroot00000000000000api2go-1.0-RC4/routing/echo.go000066400000000000000000000012651317567713700161020ustar00rootroot00000000000000// +build echo,!gorillamux,!gingonic package routing import ( "net/http" "github.com/labstack/echo" ) type echoRouter struct { echo *echo.Echo } func (e echoRouter) Handler() http.Handler { return e.echo } func (e echoRouter) Handle(protocol, route string, handler HandlerFunc) { echoHandlerFunc := func(c echo.Context) error { params := map[string]string{} for i, p := range c.ParamNames() { params[p] = c.ParamValues()[i] } handler(c.Response(), c.Request(), params) return nil } e.echo.Add(protocol, route, echoHandlerFunc) } // Echo created a new api2go router to use with the echo framework func Echo(e *echo.Echo) Routeable { return &echoRouter{echo: e} } api2go-1.0-RC4/routing/echo_test.go000066400000000000000000000076261317567713700171500ustar00rootroot00000000000000// +build echo,!gingonic,!gorillamux package routing_test import ( "io/ioutil" "log" "net/http" "net/http/httptest" "strings" "github.com/labstack/echo" "github.com/manyminds/api2go" "github.com/manyminds/api2go/examples/model" "github.com/manyminds/api2go/examples/resource" "github.com/manyminds/api2go/examples/storage" "github.com/manyminds/api2go/routing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) var _ = Describe("api2go with echo router adapter", func() { var ( router routing.Routeable e *echo.Echo api *api2go.API rec *httptest.ResponseRecorder ) BeforeSuite(func() { e = echo.New() router = routing.Echo(e) api = api2go.NewAPIWithRouting( "api", api2go.NewStaticResolver("/"), router, ) userStorage := storage.NewUserStorage() chocStorage := storage.NewChocolateStorage() api.AddResource(model.User{}, resource.UserResource{ChocStorage: chocStorage, UserStorage: userStorage}) api.AddResource(model.Chocolate{}, resource.ChocolateResource{ChocStorage: chocStorage, UserStorage: userStorage}) }) BeforeEach(func() { log.SetOutput(ioutil.Discard) rec = httptest.NewRecorder() }) Context("CRUD Tests", func() { It("will create a new user", func() { reqBody := strings.NewReader(`{"data": {"attributes": {"user-name": "Sansa Stark"}, "id": "1", "type": "users"}}`) req, err := http.NewRequest("POST", "/api/users", reqBody) Expect(err).To(BeNil()) e.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusCreated)) }) It("will find her", func() { expectedUser := ` { "data": { "attributes":{ "user-name":"Sansa Stark" }, "id":"1", "relationships":{ "sweets":{ "data":[],"links":{"related":"/api/users/1/sweets","self":"/api/users/1/relationships/sweets"} } },"type":"users" }, "meta": { "author":"The api2go examples crew","license":"wtfpl","license-url":"http://www.wtfpl.net" } }` req, err := http.NewRequest("GET", "/api/users/1", nil) Expect(err).To(BeNil()) e.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(string(rec.Body.Bytes())).To(MatchJSON((expectedUser))) }) It("can call handle", func() { handler := api.Handler() _, ok := handler.(http.Handler) Expect(ok).To(Equal(true)) }) It("update the username", func() { reqBody := strings.NewReader(`{"data": {"id": "1", "attributes": {"user-name": "Alayne"}, "type" : "users"}}`) req, err := http.NewRequest("PATCH", "/api/users/1", reqBody) Expect(err).To(BeNil()) e.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNoContent)) }) It("will find her once again", func() { expectedUser := ` { "data": { "attributes":{ "user-name":"Alayne" }, "id":"1", "relationships":{ "sweets":{ "data":[],"links":{"related":"/api/users/1/sweets","self":"/api/users/1/relationships/sweets"} } },"type":"users" }, "meta": { "author":"The api2go examples crew","license":"wtfpl","license-url":"http://www.wtfpl.net" } }` req, err := http.NewRequest("GET", "/api/users/1", nil) Expect(err).To(BeNil()) e.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(string(rec.Body.Bytes())).To(MatchJSON((expectedUser))) }) It("will delete her", func() { req, err := http.NewRequest("DELETE", "/api/users/1", nil) Expect(err).To(BeNil()) e.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNoContent)) }) It("won't find her anymore", func() { expected := `{"errors":[{"status":"404","title":"http error (404) User for id 1 not found and 0 more errors, User for id 1 not found"}]}` req, err := http.NewRequest("GET", "/api/users/1", nil) Expect(err).To(BeNil()) e.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNotFound)) Expect(string(rec.Body.Bytes())).To(MatchJSON(expected)) }) }) }) api2go-1.0-RC4/routing/gingonic.go000066400000000000000000000012231317567713700167530ustar00rootroot00000000000000// +build gingonic,!gorillamux,!echo package routing import ( "net/http" "github.com/gin-gonic/gin" ) type ginRouter struct { router *gin.Engine } func (g ginRouter) Handler() http.Handler { return g.router } func (g ginRouter) Handle(protocol, route string, handler HandlerFunc) { wrappedCallback := func(c *gin.Context) { params := map[string]string{} for _, p := range c.Params { params[p.Key] = p.Value } handler(c.Writer, c.Request, params) } g.router.Handle(protocol, route, wrappedCallback) } //Gin creates a new api2go router to use with the gin framework func Gin(g *gin.Engine) Routeable { return &ginRouter{router: g} } api2go-1.0-RC4/routing/gingonic_test.go000066400000000000000000000077041317567713700200240ustar00rootroot00000000000000// +build gingonic,!gorillamux,!echo package routing_test import ( "io/ioutil" "log" "net/http" "net/http/httptest" "strings" "github.com/gin-gonic/gin" "github.com/manyminds/api2go" "github.com/manyminds/api2go/examples/model" "github.com/manyminds/api2go/examples/resource" "github.com/manyminds/api2go/examples/storage" "github.com/manyminds/api2go/routing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) var _ = Describe("api2go with gingonic router adapter", func() { var ( router routing.Routeable gg *gin.Engine api *api2go.API rec *httptest.ResponseRecorder ) BeforeSuite(func() { gin.SetMode(gin.ReleaseMode) gg = gin.Default() router = routing.Gin(gg) api = api2go.NewAPIWithRouting( "api", api2go.NewStaticResolver("/"), router, ) userStorage := storage.NewUserStorage() chocStorage := storage.NewChocolateStorage() api.AddResource(model.User{}, resource.UserResource{ChocStorage: chocStorage, UserStorage: userStorage}) api.AddResource(model.Chocolate{}, resource.ChocolateResource{ChocStorage: chocStorage, UserStorage: userStorage}) }) BeforeEach(func() { log.SetOutput(ioutil.Discard) rec = httptest.NewRecorder() }) Context("CRUD Tests", func() { It("will create a new user", func() { reqBody := strings.NewReader(`{"data": {"attributes": {"user-name": "Sansa Stark"}, "id": "1", "type": "users"}}`) req, err := http.NewRequest("POST", "/api/users", reqBody) Expect(err).To(BeNil()) gg.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusCreated)) }) It("will find her", func() { expectedUser := ` { "data": { "attributes":{ "user-name":"Sansa Stark" }, "id":"1", "relationships":{ "sweets":{ "data":[],"links":{"related":"/api/users/1/sweets","self":"/api/users/1/relationships/sweets"} } },"type":"users" }, "meta": { "author":"The api2go examples crew","license":"wtfpl","license-url":"http://www.wtfpl.net" } }` req, err := http.NewRequest("GET", "/api/users/1", nil) Expect(err).To(BeNil()) gg.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(string(rec.Body.Bytes())).To(MatchJSON((expectedUser))) }) It("can call handle", func() { handler := api.Handler() _, ok := handler.(http.Handler) Expect(ok).To(Equal(true)) }) It("update the username", func() { reqBody := strings.NewReader(`{"data": {"id": "1", "attributes": {"user-name": "Alayne"}, "type" : "users"}}`) req, err := http.NewRequest("PATCH", "/api/users/1", reqBody) Expect(err).To(BeNil()) gg.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNoContent)) }) It("will find her once again", func() { expectedUser := ` { "data": { "attributes":{ "user-name":"Alayne" }, "id":"1", "relationships":{ "sweets":{ "data":[],"links":{"related":"/api/users/1/sweets","self":"/api/users/1/relationships/sweets"} } },"type":"users" }, "meta": { "author":"The api2go examples crew","license":"wtfpl","license-url":"http://www.wtfpl.net" } }` req, err := http.NewRequest("GET", "/api/users/1", nil) Expect(err).To(BeNil()) gg.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(string(rec.Body.Bytes())).To(MatchJSON((expectedUser))) }) It("will delete her", func() { req, err := http.NewRequest("DELETE", "/api/users/1", nil) Expect(err).To(BeNil()) gg.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNoContent)) }) It("won't find her anymore", func() { expected := `{"errors":[{"status":"404","title":"http error (404) User for id 1 not found and 0 more errors, User for id 1 not found"}]}` req, err := http.NewRequest("GET", "/api/users/1", nil) Expect(err).To(BeNil()) gg.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNotFound)) Expect(string(rec.Body.Bytes())).To(MatchJSON(expected)) }) }) }) api2go-1.0-RC4/routing/gorillamux.go000066400000000000000000000017671317567713700173560ustar00rootroot00000000000000// +build gorillamux,!gingonic,!echo package routing import ( "fmt" "net/http" "strings" "github.com/gorilla/mux" ) type gorillamuxRouter struct { router *mux.Router } func (gm gorillamuxRouter) Handler() http.Handler { return gm.router } func (gm gorillamuxRouter) Handle(protocol, route string, handler HandlerFunc) { wrappedHandler := func(w http.ResponseWriter, r *http.Request) { handler(w, r, mux.Vars(r)) } // The request path will have parameterized segments indicated as :name. Convert // that notation to the {name} notation used by Gorilla mux. orig := strings.Split(route, "/") var mod []string for _, s := range orig { if len(s) > 0 && s[0] == ':' { s = fmt.Sprintf("{%s}", s[1:]) } mod = append(mod, s) } modroute := strings.Join(mod, "/") gm.router.HandleFunc(modroute, wrappedHandler).Methods(protocol) } //Gorilla creates a new api2go router to use with the Gorilla mux framework func Gorilla(gm *mux.Router) Routeable { return &gorillamuxRouter{router: gm} } api2go-1.0-RC4/routing/gorillamux_test.go000066400000000000000000000074371317567713700204150ustar00rootroot00000000000000// +build gorillamux,!gingonic,!echo package routing_test import ( "io/ioutil" "log" "net/http" "net/http/httptest" "strings" "github.com/gorilla/mux" "github.com/manyminds/api2go" "github.com/manyminds/api2go/examples/model" "github.com/manyminds/api2go/examples/resource" "github.com/manyminds/api2go/examples/storage" "github.com/manyminds/api2go/routing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) var _ = Describe("api2go with gorillamux router adapter", func() { var ( router routing.Routeable r *mux.Router api *api2go.API rec *httptest.ResponseRecorder ) BeforeSuite(func() { r = mux.NewRouter() router = routing.Gorilla(r) api = api2go.NewAPIWithRouting( "api", api2go.NewStaticResolver("/"), router, ) userStorage := storage.NewUserStorage() chocStorage := storage.NewChocolateStorage() api.AddResource(model.User{}, resource.UserResource{ChocStorage: chocStorage, UserStorage: userStorage}) api.AddResource(model.Chocolate{}, resource.ChocolateResource{ChocStorage: chocStorage, UserStorage: userStorage}) }) BeforeEach(func() { log.SetOutput(ioutil.Discard) rec = httptest.NewRecorder() }) Context("CRUD Tests", func() { It("will create a new user", func() { reqBody := strings.NewReader(`{"data": {"attributes": {"user-name": "Sansa Stark"}, "id": "1", "type": "users"}}`) req, err := http.NewRequest("POST", "/api/users", reqBody) Expect(err).To(BeNil()) r.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusCreated)) }) It("will find her", func() { expectedUser := ` { "data": { "attributes":{ "user-name":"Sansa Stark" }, "id":"1", "relationships":{ "sweets":{ "data":[],"links":{"related":"/api/users/1/sweets","self":"/api/users/1/relationships/sweets"} } },"type":"users" }, "meta": { "author":"The api2go examples crew","license":"wtfpl","license-url":"http://www.wtfpl.net" } }` req, err := http.NewRequest("GET", "/api/users/1", nil) Expect(err).To(BeNil()) r.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(string(rec.Body.Bytes())).To(MatchJSON((expectedUser))) }) It("update the username", func() { reqBody := strings.NewReader(`{"data": {"id": "1", "attributes": {"user-name": "Alayne"}, "type" : "users"}}`) req, err := http.NewRequest("PATCH", "/api/users/1", reqBody) Expect(err).To(BeNil()) r.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNoContent)) }) It("will find her once again", func() { expectedUser := ` { "data": { "attributes":{ "user-name":"Alayne" }, "id":"1", "relationships":{ "sweets":{ "data":[],"links":{"related":"/api/users/1/sweets","self":"/api/users/1/relationships/sweets"} } },"type":"users" }, "meta": { "author":"The api2go examples crew","license":"wtfpl","license-url":"http://www.wtfpl.net" } }` req, err := http.NewRequest("GET", "/api/users/1", nil) Expect(err).To(BeNil()) r.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusOK)) Expect(string(rec.Body.Bytes())).To(MatchJSON((expectedUser))) }) It("will delete her", func() { req, err := http.NewRequest("DELETE", "/api/users/1", nil) Expect(err).To(BeNil()) r.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNoContent)) }) It("won't find her anymore", func() { expected := `{"errors":[{"status":"404","title":"http error (404) User for id 1 not found and 0 more errors, User for id 1 not found"}]}` req, err := http.NewRequest("GET", "/api/users/1", nil) Expect(err).To(BeNil()) r.ServeHTTP(rec, req) Expect(rec.Code).To(Equal(http.StatusNotFound)) Expect(string(rec.Body.Bytes())).To(MatchJSON(expected)) }) }) }) api2go-1.0-RC4/routing/httprouter.go000066400000000000000000000027711317567713700174070ustar00rootroot00000000000000package routing import ( "net/http" "github.com/julienschmidt/httprouter" ) // HTTPRouter default router implementation for api2go type HTTPRouter struct { router *httprouter.Router } // Handle each method like before and wrap them into julienschmidt handler func style func (h HTTPRouter) Handle(protocol, route string, handler HandlerFunc) { wrappedCallback := func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { params := map[string]string{} for _, p := range ps { params[p.Key] = p.Value } handler(w, r, params) } h.router.Handle(protocol, route, wrappedCallback) } // Handler returns the router func (h HTTPRouter) Handler() http.Handler { return h.router } // SetRedirectTrailingSlash wraps this internal functionality of // the julienschmidt router. func (h *HTTPRouter) SetRedirectTrailingSlash(enabled bool) { h.router.RedirectTrailingSlash = enabled } // GetRouteParameter implemention will extract the param the julienschmidt way func (h HTTPRouter) GetRouteParameter(r http.Request, param string) string { path := httprouter.CleanPath(r.URL.Path) _, params, _ := h.router.Lookup(r.Method, path) return params.ByName(param) } // NewHTTPRouter returns a new instance of julienschmidt/httprouter // this is the default router when using api2go func NewHTTPRouter(prefix string, notAllowedHandler http.Handler) Routeable { router := httprouter.New() router.HandleMethodNotAllowed = true router.MethodNotAllowed = notAllowedHandler return &HTTPRouter{router: router} } api2go-1.0-RC4/routing/router.go000066400000000000000000000015521317567713700165030ustar00rootroot00000000000000package routing import "net/http" // HandlerFunc must contain all params from the route // in the form key,value type HandlerFunc func(w http.ResponseWriter, r *http.Request, params map[string]string) // Routeable allows drop in replacement for api2go's router // by default, we are using julienschmidt/httprouter // but you can use any router that has similiar features // e.g. gin type Routeable interface { // Handler should return the routers main handler, often this is the router itself Handler() http.Handler // Handle must be implemented to register api2go's default routines // to your used router. // protocol will be PATCH,OPTIONS,GET,POST,PUT // route will be the request route /items/:id where :id means dynamically filled params // handler is the handler that will answer to this specific route Handle(protocol, route string, handler HandlerFunc) } api2go-1.0-RC4/routing/routing_suite_test.go000066400000000000000000000003021317567713700211120ustar00rootroot00000000000000package routing_test import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "testing" ) func TestRouting(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Routing Suite") } api2go-1.0-RC4/scripts/000077500000000000000000000000001317567713700146315ustar00rootroot00000000000000api2go-1.0-RC4/scripts/fmtpolice000077500000000000000000000007631317567713700165470ustar00rootroot00000000000000#!/bin/bash readonly GOPATH="${GOPATH%%:*}" main() { check_fmt check_lint } check_fmt() { eval "set -e" for file in $(git ls-files '*.go') ; do gofmt -s $file | diff -u $file - done eval "set +e" } check_lint() { for file in $(git ls-files '*.go') ; do if [[ ! "$(${GOPATH}/bin/golint $file)" =~ ^[[:blank:]]*$ ]] ; then _lint_verbose && exit 1 fi done } _lint_verbose() { for file in $(git ls-files '*.go') ; do $GOPATH/bin/golint $file ; done } main "$@"