├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gopkg.lock ├── Gopkg.toml ├── LICENCE ├── Makefile ├── README.md ├── api_key.go ├── asset.go ├── asset_test.go ├── collection.go ├── collection_test.go ├── content_type.go ├── content_type_field_validations.go ├── content_type_field_validations_test.go ├── content_type_test.go ├── contentful.go ├── contentful_test.go ├── entry.go ├── entry_field.go ├── entry_test.go ├── errors.go ├── errors_test.go ├── go.mod ├── go.sum ├── locale.go ├── locale_test.go ├── query.go ├── query_test.go ├── space.go ├── space_test.go ├── testdata ├── content_type-updated.json ├── content_type.json ├── content_type_with_validations.json ├── content_types.json ├── entry_3.json ├── error-forbidden.json ├── error-notfound.json ├── error-ratelimit.json ├── locale_1.json ├── locales.json ├── space-1.json ├── spaces-id1-assets-1x0xpXu4pSGS4OukSyWGUK.json ├── spaces-id1-assets-3HNzx9gvJScKku4UmcekYw.json ├── spaces-id1-assets-happycat.json ├── spaces-id1-assets-nyancat.json ├── spaces-id1-assets.json ├── spaces-id1-content_types-cat.json ├── spaces-id1-content_types-new.json ├── spaces-id1-entries-happycat.json ├── spaces-id1-entries-nyancat.json ├── spaces-id1-entries.json ├── spaces-id1.json ├── spaces-id2.json ├── spaces-newspace-updated.json ├── spaces-newspace.json ├── spaces-page-2.json ├── spaces.json ├── webhook-updated.json └── webhook.json ├── tools ├── lint.sh └── test.sh ├── types.go ├── version.go ├── webhook.go └── webhook_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | .cover 27 | vendor 28 | coverage.txt 29 | .idea/ 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | dist: trusty 3 | 4 | install: 5 | - make dep 6 | 7 | script: 8 | - make test 9 | - make lint 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v0.3.1 (2017-11-28) 2 | === 3 | * `~` sdk version header format fixed. 4 | 5 | v0.3.0 (2017-11-11) 6 | === 7 | * `~` use codecov as coverage service 8 | * `+` `golint` is added to the CI process 9 | * `~` `dep` is updated to the latest version 10 | * `x` `vendor` folder is not under version control 11 | * `~` `makefile` simplifications 12 | * `+` testing and linting is now handled by scripts under `tools` folder 13 | * `~` cosmetic changes in codebase to make linter happy 14 | 15 | 16 | v0.2.0 (2017-04-12) 17 | === 18 | * Godoc style examples 19 | * [Added] Query.go tests 20 | * [Added] Locale resource tests 21 | * [Added] ContentType tests 22 | * [Added] Missing space resource tests 23 | * [Added] User-Agent for api requests 24 | 25 | v0.1.1 (2017-03-31) 26 | === 27 | 28 | * [Added] Rate-limited api requests 29 | * [Fix] Locale model 30 | * [Added] Content type field unmarshaling 31 | 32 | v0.1.0 (2017-03-26) 33 | === 34 | 35 | ### Introducing resource services 36 | Every entity resource now has its own service definition for handling api communication. With this release, we don't store `contentful client` and `space` objects inside entities anymore. Resource services now get `spaceID` as a string parameter when it is neccessary. 37 | 38 | With the old versions, in order to create a new `ContentType`, for example, you first need to observe `Space` object. That is no longer required. The problem with the old method was that you had to make an extra api request to observe the `Space` in order to interact with rest of the resources. The following example demonstras the difference between old and new version. 39 | 40 | ```go 41 | // prior to v0.1.0 42 | space, err := cma.GetSpace("space-id") // this call was making an extra api call 43 | contentTypes, err := space.ContentTypes() 44 | 45 | // after v0.1.0 46 | contentType := &contentful.ContentType{ ... } 47 | spaceID := "space-id" 48 | cma.ContentTypes.Upsert(spaceID, contentType) // now we are passing spaceID as string 49 | ``` 50 | 51 | You can access available resources as follows: 52 | 53 | ```go 54 | cma := contentful.NewCMA("token") 55 | cma.Spaces 56 | cma.APIKeys 57 | cma.Assets 58 | cma.ContentTypes 59 | cma.Entries 60 | cma.Locales 61 | cma.Webhooks 62 | ``` 63 | 64 | Every resource service exposes the following interface: 65 | 66 | `List(spaceID string) *Collection` 67 | 68 | `Get(spaceID, contentTypeID string)(*ContentType, error)` 69 | 70 | `Upsert(spaceID string, ct *ContentType) error` 71 | 72 | `Delete(spaceID string, ct *ContentType) error` 73 | 74 | ### Create resource instancas directly from their model definitions 75 | 76 | All `New{ResourceName}`, such as `NewContentType`, `NewSpace`, functions are removed from the SDK. It turned out that it wasn't a good practice in golang. Instead, you can directly initiate resource entities directly from their models, such as: 77 | 78 | ```go 79 | contentType := &contentful.ContentType{ 80 | Name: "name", 81 | ... other fields 82 | } 83 | ``` 84 | 85 | 86 | v0.0.3 (2017-03-22) 87 | === 88 | * [Added] PredefinedValues validation 89 | * [Added] Range validation greater/less than equal to support. 90 | * [Added] Size validation for content type field. 91 | * [Added] Packages are vendored with `godep`. 92 | * [Added] `version.go`. 93 | * [Added] `entity/content_type`: regex validation for content type field. 94 | * [Added] Validation data structures added: `MinMax`, `Regex` 95 | * [Added] `LinkType` support for `Field` struct 96 | * [Added] New validations: `MimeType`, `Dimension`, `FileSize` 97 | 98 | 99 | v0.0.2 (2017-03-21) 100 | === 101 | * `entity/webhook`: add tests for webhook entity. 102 | * `entity/space`: add tests for space entity. 103 | * `errors`: add tests for error handler. 104 | * `entity/content_type`: add test for content type entity. 105 | * `entity/content_type`: Field validations added for link type 106 | * `entity/content_type`: field validations added: Range, PredefinedValues, Unique 107 | 108 | 109 | v0.0.1 (2017-03-20) 110 | === 111 | * `sdk`: first implementation. 112 | * `collection`: first implementation. 113 | * `entity/content_type`: first implementation. 114 | * `entity/entry`: first implementation. 115 | * `entity/query`: first implementation. 116 | * `entity/asset`: first implementation. 117 | * `entity/locale`: first implementation. 118 | * `entity/space`: first implementation. 119 | * `entity/webhook`: first implementation. 120 | * `entity/api_key`: first implementation. 121 | * `sdk`: basic documentation. 122 | * `examples`: some examples for entities 123 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b" 6 | name = "github.com/davecgh/go-spew" 7 | packages = ["spew"] 8 | pruneopts = "" 9 | revision = "346938d642f2ec3594ed81d874461961cd0faa76" 10 | version = "v1.1.0" 11 | 12 | [[projects]] 13 | digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411" 14 | name = "github.com/pmezard/go-difflib" 15 | packages = ["difflib"] 16 | pruneopts = "" 17 | revision = "792786c7400a136282c1664665ae0a8db921c6c2" 18 | version = "v1.0.0" 19 | 20 | [[projects]] 21 | digest = "1:3926a4ec9a4ff1a072458451aa2d9b98acd059a45b38f7335d31e06c3d6a0159" 22 | name = "github.com/stretchr/testify" 23 | packages = ["assert"] 24 | pruneopts = "" 25 | revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" 26 | version = "v1.1.4" 27 | 28 | [[projects]] 29 | digest = "1:a007311f74ec15ad1c56672245793e98e67566e78a0bee8b575f9856a8d5299a" 30 | name = "moul.io/http2curl" 31 | packages = ["."] 32 | pruneopts = "" 33 | revision = "5cd742060b0e0de91f875277b77dd7d7e68b23ca" 34 | version = "v2" 35 | 36 | [solve-meta] 37 | analyzer-name = "dep" 38 | analyzer-version = 1 39 | input-imports = [ 40 | "github.com/stretchr/testify/assert", 41 | "moul.io/http2curl", 42 | ] 43 | solver-name = "gps-cdcl" 44 | solver-version = 1 45 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | branch = "master" 26 | name = "github.com/moul/http2curl" 27 | 28 | [[constraint]] 29 | name = "github.com/stretchr/testify" 30 | version = "1.1.4" 31 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Contentful, Inc. 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO_CMD=go 2 | GO_BUILD=$(GO_CMD) build 3 | GO_BUILD_RACE=$(GO_CMD) build -race 4 | GO_TEST=$(GO_CMD) test 5 | GO_TEST_VERBOSE=$(GO_CMD) test -v 6 | GO_INSTALL=$(GO_CMD) install -v 7 | GO_CLEAN=$(GO_CMD) clean 8 | GO_DEPS=$(GO_CMD) get -d -v 9 | GO_DEPS_UPDATE=$(GO_CMD) get -d -v -u 10 | GO_VET=$(GO_CMD) vet 11 | GO_FMT=$(GO_CMD) fmt 12 | 13 | .PHONY: all test lint dep 14 | 15 | all: dep test lint 16 | 17 | test: 18 | ./tools/test.sh 19 | 20 | lint: 21 | ./tools/lint.sh 22 | 23 | dep: 24 | curl -fsSL -o /tmp/dep https://github.com/golang/dep/releases/download/v0.3.2/dep-linux-amd64 25 | chmod +x /tmp/dep 26 | /tmp/dep ensure -vendor-only 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Godoc](https://img.shields.io/badge/godoc-Reference-brightgreen.svg?style=flat)](https://godoc.org/github.com/contentful-labs/contentful-go) 2 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 3 | [![Build Status](https://travis-ci.com/contentful-labs/contentful-go.svg?token=ppF3HxXy28XU9AwHHiGX&branch=master)](https://travis-ci.com/contentful-labs/contentful-go) 4 | 5 | ❗ Disclaimer 6 | ===== 7 | 8 | **This project is not actively maintained or monitored.** Feel free to fork and work on it in your account. If you want to maintain but also collaborate with fellow developers, feel free to reach out to [Contentful's Developer Relations](mailto:devrel-mkt@contentful.com) team to move the project into our community GitHub organisation [contentful-userland](https://github.com/contentful-userland/). 9 | 10 | # contentful-go 11 | 12 | GoLang SDK for [Contentful's](https://www.contentful.com) Content Delivery, Preview and Management API's. 13 | 14 | # About 15 | 16 | [Contentful](https://www.contentful.com) provides a content infrastructure for digital teams to power content in websites, apps, and devices. Unlike a CMS, Contentful was built to integrate with the modern software stack. It offers a central hub for structured content, powerful management and delivery APIs, and a customizable web app that enable developers and content creators to ship digital products faster. 17 | 18 | [Go](https://golang.org) is an open source programming language that makes it easy to build simple, reliable, and efficient software. 19 | 20 | # Install 21 | 22 | `go get github.com/contentful-labs/contentful-go` 23 | 24 | # Getting started 25 | 26 | Import into your Go project or library 27 | 28 | ```go 29 | import ( 30 | contentful "github.com/contentful-labs/contentful-go" 31 | ) 32 | ``` 33 | 34 | Create a API client in order to interact with the Contentful's API endpoints. 35 | 36 | ```go 37 | token := "your-cma-token" // observe your CMA token from Contentful's web page 38 | cma := contentful.NewCMA(token) 39 | ``` 40 | 41 | #### Organization 42 | 43 | If your Contentful account is part of an organization, you can setup your API client as so. When you set your organization id for the SDK client, every api request will have `X-Contentful-Organization: ` header automatically. 44 | 45 | ```go 46 | cma.SetOrganization("your-organization-id") 47 | ``` 48 | 49 | #### Debug mode 50 | 51 | When debug mode is activated, sdk client starts to work in verbose mode and try to print as much informatin as possible. In debug mode, all outgoing http requests are printed nicely in the form of `curl` command so that you can easly drop into your command line to debug specific request. 52 | 53 | ```go 54 | cma.Debug = true 55 | ``` 56 | 57 | #### Dependencies 58 | 59 | `contentful-go` stores its dependencies under `vendor` folder and uses [`dep`](https://github.com/golang/dep) to manage dependency resolutions. Dependencies in `vendor` folder will be loaded automatically by [Go 1.6+](https://golang.org/cmd/go/#hdr-Vendor_Directories). To install the dependencies, run `dep ensure`, for more options and documentation please visit [`dep`](https://github.com/golang/dep). 60 | 61 | # Using the SDK 62 | 63 | ## Working with resource services 64 | 65 | Currently SDK exposes the following resource services: 66 | 67 | * Spaces 68 | * APIKeys 69 | * Assets 70 | * ContentTypes 71 | * Entries 72 | * Locales 73 | * Webhooks 74 | 75 | Every resource service has at least the following interface: 76 | 77 | ```go 78 | List() *Collection 79 | Get(spaceID, resourceID string) , error 80 | Upsert(spaceID string, resourceID *Resource) error 81 | Delete(spaceID string, resourceID *Resource) error 82 | ``` 83 | 84 | #### Example 85 | 86 | ```go 87 | space, err := cma.Spaces.Get("space-id") 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | 92 | collection := cma.ContentTypes.List(space.Sys.ID) 93 | collection, err = collection.Next() 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | 98 | for _, contentType := range collection.ToContentType() { 99 | fmt.Println(contentType.Name, contentType.Description) 100 | } 101 | ``` 102 | 103 | ## Working with collections 104 | 105 | All the endpoints which return an array of objects are wrapped around `Collection` struct. The main features of `Collection` are pagination and type assertion. 106 | 107 | ### Pagination 108 | WIP 109 | 110 | ### Type assertion 111 | 112 | `Collection` struct exposes the necessary converters (type assertion) such as `ToSpace()`. The following example gets all spaces for the given account: 113 | 114 | ### Example 115 | 116 | ```go 117 | collection := cma.Spaces.List() // returns a collection 118 | collection, err := collection.Next() // makes the actual api call 119 | if err != nil { 120 | log.Fatal(err) 121 | } 122 | 123 | spaces := collection.ToSpace() // make the type assertion 124 | for _, space := range spaces { 125 | fmt.Println(space.Name) 126 | fmt.Println(space.Sys.ID) 127 | } 128 | 129 | // In order to access collection metadata 130 | fmt.Println(col.Total) 131 | fmt.Println(col.Skip) 132 | fmt.Println(col.Limit) 133 | ``` 134 | 135 | ## Testing 136 | 137 | ```shell 138 | $> go test 139 | ``` 140 | 141 | To enable higher verbose mode 142 | 143 | ```shell 144 | $> go test -v -race 145 | ``` 146 | 147 | ## Documentation/References 148 | 149 | ### Contentful 150 | [Content Delivery API](https://www.contentful.com/developers/docs/references/content-delivery-api/) 151 | [Content Management API](https://www.contentful.com/developers/docs/references/content-management-api/) 152 | [Content Preview API](https://www.contentful.com/developers/docs/references/content-preview-api/) 153 | 154 | ### GoLang 155 | [Effective Go](https://golang.org/doc/effective_go.html) 156 | 157 | ## Support 158 | 159 | This is a project created for demo purposes and not officially supported, so if you find issues or have questions you can let us know via the [issue](https://github.com/contentful-labs/contentful-go/issues/new) page but don't expect a quick and prompt response. 160 | 161 | ## Contributing 162 | 163 | [WIP] 164 | 165 | ## License 166 | 167 | MIT 168 | -------------------------------------------------------------------------------- /api_key.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "strconv" 8 | ) 9 | 10 | // APIKeyService service 11 | type APIKeyService service 12 | 13 | // APIKey model 14 | type APIKey struct { 15 | Sys *Sys `json:"sys,omitempty"` 16 | Name string `json:"name,omitempty"` 17 | Description string `json:"description,omitempty"` 18 | AccessToken string `json:"accessToken,omitempty"` 19 | Policies []*APIKeyPolicy `json:"policies,omitempty"` 20 | PreviewAPIKey *PreviewAPIKey `json:"preview_api_key,omitempty"` 21 | } 22 | 23 | // APIKeyPolicy model 24 | type APIKeyPolicy struct { 25 | Effect string `json:"effect,omitempty"` 26 | Actions string `json:"actions,omitempty"` 27 | } 28 | 29 | // PreviewAPIKey model 30 | type PreviewAPIKey struct { 31 | Sys *Sys 32 | } 33 | 34 | // MarshalJSON for custom json marshaling 35 | func (apiKey *APIKey) MarshalJSON() ([]byte, error) { 36 | return json.Marshal(&struct { 37 | Name string `json:"name"` 38 | Description string `json:"description,omitempty"` 39 | }{ 40 | Name: apiKey.Name, 41 | Description: apiKey.Description, 42 | }) 43 | } 44 | 45 | // GetVersion returns entity version 46 | func (apiKey *APIKey) GetVersion() int { 47 | version := 1 48 | if apiKey.Sys != nil { 49 | version = apiKey.Sys.Version 50 | } 51 | 52 | return version 53 | } 54 | 55 | // List returns all api keys collection 56 | func (service *APIKeyService) List(spaceID string) *Collection { 57 | path := fmt.Sprintf("/spaces/%s/api_keys", spaceID) 58 | method := "GET" 59 | 60 | req, err := service.c.newRequest(method, path, nil, nil) 61 | if err != nil { 62 | return &Collection{} 63 | } 64 | 65 | col := NewCollection(&CollectionOptions{}) 66 | col.c = service.c 67 | col.req = req 68 | 69 | return col 70 | } 71 | 72 | // Get returns a single api key entity 73 | func (service *APIKeyService) Get(spaceID, apiKeyID string) (*APIKey, error) { 74 | path := fmt.Sprintf("/spaces/%s/api_keys/%s", spaceID, apiKeyID) 75 | method := "GET" 76 | 77 | req, err := service.c.newRequest(method, path, nil, nil) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | var apiKey APIKey 83 | if err := service.c.do(req, &apiKey); err != nil { 84 | return nil, err 85 | } 86 | 87 | return &apiKey, nil 88 | } 89 | 90 | // Upsert updates or creates a new api key entity 91 | func (service *APIKeyService) Upsert(spaceID string, apiKey *APIKey) error { 92 | bytesArray, err := json.Marshal(apiKey) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | var path string 98 | var method string 99 | 100 | if apiKey.Sys != nil && apiKey.Sys.CreatedAt != "" { 101 | path = fmt.Sprintf("/spaces/%s/api_keys/%s", spaceID, apiKey.Sys.ID) 102 | method = "PUT" 103 | } else { 104 | path = fmt.Sprintf("/spaces/%s/api_keys", spaceID) 105 | method = "POST" 106 | } 107 | 108 | req, err := service.c.newRequest(method, path, nil, bytes.NewReader(bytesArray)) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | req.Header.Set("X-Contentful-Version", strconv.Itoa(apiKey.GetVersion())) 114 | 115 | return service.c.do(req, apiKey) 116 | } 117 | 118 | // Delete deletes a sinlge api key entity 119 | func (service *APIKeyService) Delete(spaceID string, apiKey *APIKey) error { 120 | path := fmt.Sprintf("/spaces/%s/api_keys/%s", spaceID, apiKey.Sys.ID) 121 | method := "DELETE" 122 | 123 | req, err := service.c.newRequest(method, path, nil, nil) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | version := strconv.Itoa(apiKey.Sys.Version) 129 | req.Header.Set("X-Contentful-Version", version) 130 | 131 | return service.c.do(req, nil) 132 | } 133 | -------------------------------------------------------------------------------- /asset.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "strconv" 8 | ) 9 | 10 | // AssetsService service 11 | type AssetsService service 12 | 13 | // File model 14 | type File struct { 15 | Name string `json:"fileName,omitempty"` 16 | ContentType string `json:"contentType,omitempty"` 17 | URL string `json:"url,omitempty"` 18 | UploadURL string `json:"upload,omitempty"` 19 | Detail *FileDetail `json:"details,omitempty"` 20 | } 21 | 22 | // FileDetail model 23 | type FileDetail struct { 24 | Size int `json:"size,omitempty"` 25 | Image *FileImage `json:"image,omitempty"` 26 | } 27 | 28 | // FileImage model 29 | type FileImage struct { 30 | Width int `json:"width,omitempty"` 31 | Height int `json:"height,omitempty"` 32 | } 33 | 34 | // FileFields model 35 | type FileFields struct { 36 | Title string `json:"title,omitempty"` 37 | Description string `json:"description,omitempty"` 38 | File *File `json:"file,omitempty"` 39 | } 40 | 41 | // Asset model 42 | type Asset struct { 43 | locale string 44 | Sys *Sys `json:"sys"` 45 | Fields *FileFields `json:"fields"` 46 | } 47 | 48 | // MarshalJSON for custom json marshaling 49 | func (asset *Asset) MarshalJSON() ([]byte, error) { 50 | payload := map[string]interface{}{ 51 | "sys": "", 52 | "fields": map[string]interface{}{ 53 | "title": map[string]string{}, 54 | "description": map[string]string{}, 55 | "file": map[string]interface{}{}, 56 | }, 57 | } 58 | 59 | payload["sys"] = asset.Sys 60 | fields := payload["fields"].(map[string]interface{}) 61 | 62 | // title 63 | title := fields["title"].(map[string]string) 64 | title[asset.locale] = asset.Fields.Title 65 | 66 | // description 67 | description := fields["description"].(map[string]string) 68 | description[asset.locale] = asset.Fields.Description 69 | 70 | // file 71 | file := fields["file"].(map[string]interface{}) 72 | file[asset.locale] = asset.Fields.File 73 | 74 | return json.Marshal(payload) 75 | } 76 | 77 | // UnmarshalJSON for custom json unmarshaling 78 | func (asset *Asset) UnmarshalJSON(data []byte) error { 79 | type Alias *Asset 80 | 81 | var payload map[string]interface{} 82 | if err := json.Unmarshal(data, &payload); err != nil { 83 | return err 84 | } 85 | 86 | fileName := payload["fields"].(map[string]interface{})["file"].(map[string]interface{})["fileName"] 87 | localized := true 88 | 89 | if fileName == nil { 90 | localized = false 91 | } 92 | 93 | if localized == false { 94 | asset.Sys = &Sys{} 95 | b, _ := json.Marshal(payload["sys"]) 96 | if err := json.Unmarshal(b, asset.Sys); err != nil { 97 | return err 98 | } 99 | 100 | title := payload["fields"].(map[string]interface{})["title"] 101 | if title != nil { 102 | title = title.(map[string]interface{})[asset.locale] 103 | } 104 | 105 | description := payload["fields"].(map[string]interface{})["description"] 106 | if description != nil { 107 | description = description.(map[string]interface{})[asset.locale] 108 | } 109 | 110 | asset.Fields = &FileFields{ 111 | Title: title.(string), 112 | Description: description.(string), 113 | File: &File{}, 114 | } 115 | 116 | file := payload["fields"].(map[string]interface{})["file"].(map[string]interface{})[asset.locale] 117 | if err := json.Unmarshal([]byte(file.(string)), asset.Fields.File); err != nil { 118 | return err 119 | } 120 | } else { 121 | if err := json.Unmarshal(data, Alias(asset)); err != nil { 122 | return err 123 | } 124 | } 125 | 126 | return nil 127 | } 128 | 129 | // GetVersion returns entity version 130 | func (asset *Asset) GetVersion() int { 131 | version := 1 132 | if asset.Sys != nil { 133 | version = asset.Sys.Version 134 | } 135 | 136 | return version 137 | } 138 | 139 | // List returns asset collection 140 | func (service *AssetsService) List(spaceID string) *Collection { 141 | path := fmt.Sprintf("/spaces/%s/assets", spaceID) 142 | method := "GET" 143 | 144 | req, err := service.c.newRequest(method, path, nil, nil) 145 | if err != nil { 146 | return &Collection{} 147 | } 148 | 149 | col := NewCollection(&CollectionOptions{}) 150 | col.c = service.c 151 | col.req = req 152 | 153 | return col 154 | } 155 | 156 | // Get returns a single asset entity 157 | func (service *AssetsService) Get(spaceID, assetID string) (*Asset, error) { 158 | path := fmt.Sprintf("/spaces/%s/assets/%s", spaceID, assetID) 159 | method := "GET" 160 | 161 | req, err := service.c.newRequest(method, path, nil, nil) 162 | if err != nil { 163 | return nil, err 164 | } 165 | 166 | var asset Asset 167 | if err := service.c.do(req, &asset); err != nil { 168 | return nil, err 169 | } 170 | 171 | return &asset, nil 172 | } 173 | 174 | // Upsert updates or creates a new asset entity 175 | func (service *AssetsService) Upsert(spaceID string, asset *Asset) error { 176 | bytesArray, err := json.Marshal(asset) 177 | if err != nil { 178 | return err 179 | } 180 | 181 | var path string 182 | var method string 183 | 184 | if asset.Sys.CreatedAt != "" { 185 | path = fmt.Sprintf("/spaces/%s/assets/%s", spaceID, asset.Sys.ID) 186 | method = "PUT" 187 | } else { 188 | path = fmt.Sprintf("/spaces/%s/assets", spaceID) 189 | method = "POST" 190 | } 191 | 192 | req, err := service.c.newRequest(method, path, nil, bytes.NewReader(bytesArray)) 193 | if err != nil { 194 | return err 195 | } 196 | 197 | req.Header.Set("X-Contentful-Version", strconv.Itoa(asset.GetVersion())) 198 | 199 | return service.c.do(req, asset) 200 | } 201 | 202 | // Delete sends delete request 203 | func (service *AssetsService) Delete(spaceID string, asset *Asset) error { 204 | path := fmt.Sprintf("/spaces/%s/assets/%s", spaceID, asset.Sys.ID) 205 | method := "DELETE" 206 | 207 | req, err := service.c.newRequest(method, path, nil, nil) 208 | if err != nil { 209 | return err 210 | } 211 | 212 | version := strconv.Itoa(asset.Sys.Version) 213 | req.Header.Set("X-Contentful-Version", version) 214 | 215 | return service.c.do(req, nil) 216 | } 217 | 218 | // Process the asset 219 | func (service *AssetsService) Process(spaceID string, asset *Asset) error { 220 | path := fmt.Sprintf("/spaces/%s/assets/%s/files/%s/process", spaceID, asset.Sys.ID, asset.locale) 221 | method := "PUT" 222 | 223 | req, err := service.c.newRequest(method, path, nil, nil) 224 | if err != nil { 225 | return err 226 | } 227 | 228 | version := strconv.Itoa(asset.Sys.Version) 229 | req.Header.Set("X-Contentful-Version", version) 230 | 231 | return service.c.do(req, nil) 232 | } 233 | 234 | // Publish published the asset 235 | func (service *AssetsService) Publish(spaceID string, asset *Asset) error { 236 | path := fmt.Sprintf("/spaces/%s/assets/%s/published", spaceID, asset.Sys.ID) 237 | method := "PUT" 238 | 239 | req, err := service.c.newRequest(method, path, nil, nil) 240 | if err != nil { 241 | return err 242 | } 243 | 244 | version := strconv.Itoa(asset.Sys.Version) 245 | req.Header.Set("X-Contentful-Version", version) 246 | 247 | return service.c.do(req, asset) 248 | } 249 | -------------------------------------------------------------------------------- /asset_test.go: -------------------------------------------------------------------------------- 1 | package contentful -------------------------------------------------------------------------------- /collection.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | ) 8 | 9 | // CollectionOptions holds init options 10 | type CollectionOptions struct { 11 | Limit uint16 12 | } 13 | 14 | // Collection model 15 | type Collection struct { 16 | Query 17 | c *Client 18 | req *http.Request 19 | page uint16 20 | Sys *Sys `json:"sys"` 21 | Total int `json:"total"` 22 | Skip int `json:"skip"` 23 | Limit int `json:"limit"` 24 | Items []interface{} `json:"items"` 25 | Includes interface{} `json:"includes"` 26 | } 27 | 28 | // NewCollection initilazies a new collection 29 | func NewCollection(options *CollectionOptions) *Collection { 30 | query := NewQuery() 31 | query.Order("sys.createdAt", true) 32 | 33 | if options.Limit > 0 { 34 | query.Limit(options.Limit) 35 | } 36 | 37 | return &Collection{ 38 | Query: *query, 39 | page: 1, 40 | } 41 | } 42 | 43 | // Next makes the col.req 44 | func (col *Collection) Next() (*Collection, error) { 45 | // setup query params 46 | skip := uint16(col.Limit) * (col.page - 1) 47 | col.Query.Skip(skip) 48 | 49 | // override request query 50 | col.req.URL.RawQuery = col.Query.String() 51 | 52 | // makes api call 53 | err := col.c.do(col.req, col) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | col.page++ 59 | 60 | return col, nil 61 | } 62 | 63 | // ToContentType cast Items to ContentType model 64 | func (col *Collection) ToContentType() []*ContentType { 65 | var contentTypes []*ContentType 66 | 67 | byteArray, _ := json.Marshal(col.Items) 68 | json.NewDecoder(bytes.NewReader(byteArray)).Decode(&contentTypes) 69 | 70 | return contentTypes 71 | } 72 | 73 | // ToSpace cast Items to Space model 74 | func (col *Collection) ToSpace() []*Space { 75 | var spaces []*Space 76 | 77 | byteArray, _ := json.Marshal(col.Items) 78 | json.NewDecoder(bytes.NewReader(byteArray)).Decode(&spaces) 79 | 80 | return spaces 81 | } 82 | 83 | // ToEntry cast Items to Entry model 84 | func (col *Collection) ToEntry() []*Entry { 85 | var entries []*Entry 86 | 87 | byteArray, _ := json.Marshal(col.Items) 88 | json.NewDecoder(bytes.NewReader(byteArray)).Decode(&entries) 89 | 90 | return entries 91 | } 92 | 93 | // ToLocale cast Items to Locale model 94 | func (col *Collection) ToLocale() []*Locale { 95 | var locales []*Locale 96 | 97 | byteArray, _ := json.Marshal(col.Items) 98 | json.NewDecoder(bytes.NewReader(byteArray)).Decode(&locales) 99 | 100 | return locales 101 | } 102 | 103 | // ToAsset cast Items to Asset model 104 | func (col *Collection) ToAsset() []*Asset { 105 | var assets []*Asset 106 | 107 | byteArray, _ := json.Marshal(col.Items) 108 | json.NewDecoder(bytes.NewReader(byteArray)).Decode(&assets) 109 | 110 | return assets 111 | } 112 | 113 | // ToAPIKey cast Items to APIKey model 114 | func (col *Collection) ToAPIKey() []*APIKey { 115 | var apiKeys []*APIKey 116 | 117 | byteArray, _ := json.Marshal(col.Items) 118 | json.NewDecoder(bytes.NewReader(byteArray)).Decode(&apiKeys) 119 | 120 | return apiKeys 121 | } 122 | 123 | // ToWebhook cast Items to Webhook model 124 | func (col *Collection) ToWebhook() []*Webhook { 125 | var webhooks []*Webhook 126 | 127 | byteArray, _ := json.Marshal(col.Items) 128 | json.NewDecoder(bytes.NewReader(byteArray)).Decode(&webhooks) 129 | 130 | return webhooks 131 | } 132 | -------------------------------------------------------------------------------- /collection_test.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import "testing" 4 | 5 | func TestNewCollection(t *testing.T) { 6 | setup() 7 | defer teardown() 8 | } 9 | -------------------------------------------------------------------------------- /content_type.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "strconv" 8 | ) 9 | 10 | // ContentTypesService service 11 | type ContentTypesService service 12 | 13 | // ContentType model 14 | type ContentType struct { 15 | Sys *Sys `json:"sys"` 16 | Name string `json:"name,omitempty"` 17 | Description string `json:"description,omitempty"` 18 | Fields []*Field `json:"fields,omitempty"` 19 | DisplayField string `json:"displayField,omitempty"` 20 | } 21 | 22 | const ( 23 | // FieldTypeText content type field type for text data 24 | FieldTypeText = "Text" 25 | 26 | // FieldTypeSymbol content type field type for text data 27 | FieldTypeSymbol = "Symbol" 28 | 29 | // FieldTypeArray content type field type for array data 30 | FieldTypeArray = "Array" 31 | 32 | // FieldTypeLink content type field type for link data 33 | FieldTypeLink = "Link" 34 | 35 | // FieldTypeInteger content type field type for integer data 36 | FieldTypeInteger = "Integer" 37 | 38 | // FieldTypeLocation content type field type for location data 39 | FieldTypeLocation = "Location" 40 | 41 | // FieldTypeBoolean content type field type for boolean data 42 | FieldTypeBoolean = "Boolean" 43 | 44 | // FieldTypeDate content type field type for date data 45 | FieldTypeDate = "Date" 46 | 47 | // FieldTypeObject content type field type for object data 48 | FieldTypeObject = "Object" 49 | ) 50 | 51 | // Field model 52 | type Field struct { 53 | ID string `json:"id,omitempty"` 54 | Name string `json:"name"` 55 | Type string `json:"type"` 56 | LinkType string `json:"linkType,omitempty"` 57 | Items *FieldTypeArrayItem `json:"items,omitempty"` 58 | Required bool `json:"required,omitempty"` 59 | Localized bool `json:"localized,omitempty"` 60 | Disabled bool `json:"disabled,omitempty"` 61 | Omitted bool `json:"omitted,omitempty"` 62 | Validations []FieldValidation `json:"validations,omitempty"` 63 | } 64 | 65 | // UnmarshalJSON for custom json unmarshaling 66 | func (field *Field) UnmarshalJSON(data []byte) error { 67 | payload := map[string]interface{}{} 68 | if err := json.Unmarshal(data, &payload); err != nil { 69 | return err 70 | } 71 | 72 | if val, ok := payload["id"]; ok { 73 | field.ID = val.(string) 74 | } 75 | 76 | if val, ok := payload["name"]; ok { 77 | field.Name = val.(string) 78 | } 79 | 80 | if val, ok := payload["type"]; ok { 81 | field.Type = val.(string) 82 | } 83 | 84 | if val, ok := payload["linkType"]; ok { 85 | field.LinkType = val.(string) 86 | } 87 | 88 | if val, ok := payload["items"]; ok { 89 | byteArray, err := json.Marshal(val) 90 | if err != nil { 91 | return nil 92 | } 93 | 94 | var fieldTypeArrayItem FieldTypeArrayItem 95 | if err := json.Unmarshal(byteArray, &fieldTypeArrayItem); err != nil { 96 | return err 97 | } 98 | 99 | field.Items = &fieldTypeArrayItem 100 | } 101 | 102 | if val, ok := payload["required"]; ok { 103 | field.Required = val.(bool) 104 | } 105 | 106 | if val, ok := payload["localized"]; ok { 107 | field.Localized = val.(bool) 108 | } 109 | 110 | if val, ok := payload["disabled"]; ok { 111 | field.Disabled = val.(bool) 112 | } 113 | 114 | if val, ok := payload["omitted"]; ok { 115 | field.Omitted = val.(bool) 116 | } 117 | 118 | if val, ok := payload["validations"]; ok { 119 | validations, err := ParseValidations(val.([]interface{})) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | field.Validations = validations 125 | } 126 | 127 | return nil 128 | } 129 | 130 | // ParseValidations converts json representation to go struct 131 | func ParseValidations(data []interface{}) (validations []FieldValidation, err error) { 132 | for _, value := range data { 133 | var validation map[string]interface{} 134 | var byteArray []byte 135 | 136 | if validationStr, ok := value.(string); ok { 137 | if err := json.Unmarshal([]byte(validationStr), &validation); err != nil { 138 | return nil, err 139 | } 140 | 141 | byteArray = []byte(validationStr) 142 | } 143 | 144 | if validationMap, ok := value.(map[string]interface{}); ok { 145 | byteArray, err = json.Marshal(validationMap) 146 | if err != nil { 147 | return nil, err 148 | } 149 | 150 | validation = validationMap 151 | } 152 | 153 | if _, ok := validation["linkContentType"]; ok { 154 | var fieldValidationLink FieldValidationLink 155 | if err := json.Unmarshal(byteArray, &fieldValidationLink); err != nil { 156 | return nil, err 157 | } 158 | 159 | validations = append(validations, fieldValidationLink) 160 | } 161 | 162 | if _, ok := validation["linkMimetypeGroup"]; ok { 163 | var fieldValidationMimeType FieldValidationMimeType 164 | if err := json.Unmarshal(byteArray, &fieldValidationMimeType); err != nil { 165 | return nil, err 166 | } 167 | 168 | validations = append(validations, fieldValidationMimeType) 169 | } 170 | 171 | if _, ok := validation["assetImageDimensions"]; ok { 172 | var fieldValidationDimension FieldValidationDimension 173 | if err := json.Unmarshal(byteArray, &fieldValidationDimension); err != nil { 174 | return nil, err 175 | } 176 | 177 | validations = append(validations, fieldValidationDimension) 178 | } 179 | 180 | if _, ok := validation["assetFileSize"]; ok { 181 | var fieldValidationFileSize FieldValidationFileSize 182 | if err := json.Unmarshal(byteArray, &fieldValidationFileSize); err != nil { 183 | return nil, err 184 | } 185 | 186 | validations = append(validations, fieldValidationFileSize) 187 | } 188 | 189 | if _, ok := validation["unique"]; ok { 190 | var fieldValidationUnique FieldValidationUnique 191 | if err := json.Unmarshal(byteArray, &fieldValidationUnique); err != nil { 192 | return nil, err 193 | } 194 | 195 | validations = append(validations, fieldValidationUnique) 196 | } 197 | 198 | if _, ok := validation["in"]; ok { 199 | var fieldValidationPredefinedValues FieldValidationPredefinedValues 200 | if err := json.Unmarshal(byteArray, &fieldValidationPredefinedValues); err != nil { 201 | return nil, err 202 | } 203 | 204 | validations = append(validations, fieldValidationPredefinedValues) 205 | } 206 | 207 | if _, ok := validation["range"]; ok { 208 | var fieldValidationRange FieldValidationRange 209 | if err := json.Unmarshal(byteArray, &fieldValidationRange); err != nil { 210 | return nil, err 211 | } 212 | 213 | validations = append(validations, fieldValidationRange) 214 | } 215 | 216 | if _, ok := validation["dateRange"]; ok { 217 | var fieldValidationDate FieldValidationDate 218 | if err := json.Unmarshal(byteArray, &fieldValidationDate); err != nil { 219 | return nil, err 220 | } 221 | 222 | validations = append(validations, fieldValidationDate) 223 | } 224 | 225 | if _, ok := validation["size"]; ok { 226 | var fieldValidationSize FieldValidationSize 227 | if err := json.Unmarshal(byteArray, &fieldValidationSize); err != nil { 228 | return nil, err 229 | } 230 | 231 | validations = append(validations, fieldValidationSize) 232 | } 233 | 234 | if _, ok := validation["regexp"]; ok { 235 | var fieldValidationRegex FieldValidationRegex 236 | if err := json.Unmarshal(byteArray, &fieldValidationRegex); err != nil { 237 | return nil, err 238 | } 239 | 240 | validations = append(validations, fieldValidationRegex) 241 | } 242 | } 243 | 244 | return validations, nil 245 | } 246 | 247 | // FieldTypeArrayItem model 248 | type FieldTypeArrayItem struct { 249 | Type string `json:"type,omitempty"` 250 | Validations []FieldValidation `json:"validations,omitempty"` 251 | LinkType string `json:"linkType,omitempty"` 252 | } 253 | 254 | // UnmarshalJSON for custom json unmarshaling 255 | func (item *FieldTypeArrayItem) UnmarshalJSON(data []byte) error { 256 | payload := map[string]interface{}{} 257 | if err := json.Unmarshal(data, &payload); err != nil { 258 | return err 259 | } 260 | 261 | if val, ok := payload["type"]; ok { 262 | item.Type = val.(string) 263 | } 264 | 265 | if val, ok := payload["validations"]; ok { 266 | validations, err := ParseValidations(val.([]interface{})) 267 | if err != nil { 268 | return err 269 | } 270 | 271 | item.Validations = validations 272 | } 273 | 274 | if val, ok := payload["linktype"]; ok { 275 | item.LinkType = val.(string) 276 | } 277 | 278 | return nil 279 | } 280 | 281 | // GetVersion returns entity version 282 | func (ct *ContentType) GetVersion() int { 283 | version := 1 284 | if ct.Sys != nil { 285 | version = ct.Sys.Version 286 | } 287 | 288 | return version 289 | } 290 | 291 | // List return a content type collection 292 | func (service *ContentTypesService) List(spaceID string) *Collection { 293 | path := fmt.Sprintf("/spaces/%s/content_types", spaceID) 294 | method := "GET" 295 | 296 | req, err := service.c.newRequest(method, path, nil, nil) 297 | if err != nil { 298 | return nil 299 | } 300 | 301 | col := NewCollection(&CollectionOptions{}) 302 | col.c = service.c 303 | col.req = req 304 | 305 | return col 306 | } 307 | 308 | // Get fetched a content type specified by `contentTypeID` 309 | func (service *ContentTypesService) Get(spaceID, contentTypeID string) (*ContentType, error) { 310 | path := fmt.Sprintf("/spaces/%s/content_types/%s", spaceID, contentTypeID) 311 | method := "GET" 312 | 313 | req, err := service.c.newRequest(method, path, nil, nil) 314 | if err != nil { 315 | return nil, err 316 | } 317 | 318 | var ct ContentType 319 | if err = service.c.do(req, &ct); err != nil { 320 | return nil, err 321 | } 322 | 323 | return &ct, nil 324 | } 325 | 326 | // Upsert updates or creates a new content type 327 | func (service *ContentTypesService) Upsert(spaceID string, ct *ContentType) error { 328 | bytesArray, err := json.Marshal(ct) 329 | if err != nil { 330 | return err 331 | } 332 | 333 | var path string 334 | var method string 335 | 336 | if ct.Sys != nil && ct.Sys.ID != "" { 337 | path = fmt.Sprintf("/spaces/%s/content_types/%s", spaceID, ct.Sys.ID) 338 | method = "PUT" 339 | } else { 340 | path = fmt.Sprintf("/spaces/%s/content_types", spaceID) 341 | method = "POST" 342 | } 343 | 344 | req, err := service.c.newRequest(method, path, nil, bytes.NewReader(bytesArray)) 345 | if err != nil { 346 | return err 347 | } 348 | 349 | req.Header.Set("X-Contentful-Version", strconv.Itoa(ct.GetVersion())) 350 | 351 | return service.c.do(req, ct) 352 | } 353 | 354 | // Delete the content_type 355 | func (service *ContentTypesService) Delete(spaceID string, ct *ContentType) error { 356 | path := fmt.Sprintf("/spaces/%s/content_types/%s", spaceID, ct.Sys.ID) 357 | method := "DELETE" 358 | 359 | req, err := service.c.newRequest(method, path, nil, nil) 360 | if err != nil { 361 | return err 362 | } 363 | 364 | version := strconv.Itoa(ct.Sys.Version) 365 | req.Header.Set("X-Contentful-Version", version) 366 | 367 | return service.c.do(req, nil) 368 | } 369 | 370 | // Activate the contenttype, a.k.a publish 371 | func (service *ContentTypesService) Activate(spaceID string, ct *ContentType) error { 372 | path := fmt.Sprintf("/spaces/%s/content_types/%s/published", spaceID, ct.Sys.ID) 373 | method := "PUT" 374 | 375 | req, err := service.c.newRequest(method, path, nil, nil) 376 | if err != nil { 377 | return err 378 | } 379 | 380 | version := strconv.Itoa(ct.Sys.Version) 381 | req.Header.Set("X-Contentful-Version", version) 382 | 383 | return service.c.do(req, ct) 384 | } 385 | 386 | // Deactivate the contenttype, a.k.a unpublish 387 | func (service *ContentTypesService) Deactivate(spaceID string, ct *ContentType) error { 388 | path := fmt.Sprintf("/spaces/%s/content_types/%s/published", spaceID, ct.Sys.ID) 389 | method := "DELETE" 390 | 391 | req, err := service.c.newRequest(method, path, nil, nil) 392 | if err != nil { 393 | return err 394 | } 395 | 396 | version := strconv.Itoa(ct.Sys.Version) 397 | req.Header.Set("X-Contentful-Version", version) 398 | 399 | return service.c.do(req, ct) 400 | } 401 | -------------------------------------------------------------------------------- /content_type_field_validations.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // FieldValidation interface 9 | type FieldValidation interface{} 10 | 11 | // FieldValidationLink model 12 | type FieldValidationLink struct { 13 | LinkContentType []string `json:"linkContentType,omitempty"` 14 | } 15 | 16 | const ( 17 | // MimeTypeAttachment mime type validation for content type field 18 | MimeTypeAttachment = "attachment" 19 | 20 | // MimeTypePlainText mime type validation for content type field 21 | MimeTypePlainText = "plaintext" 22 | 23 | // MimeTypeImage mime type validation for content type field 24 | MimeTypeImage = "image" 25 | 26 | // MimeTypeAudio mime type validation for content type field 27 | MimeTypeAudio = "audio" 28 | 29 | // MimeTypeVideo mime type validation for content type field 30 | MimeTypeVideo = "video" 31 | 32 | // MimeTypeRichText mime type validation for content type field 33 | MimeTypeRichText = "richtext" 34 | 35 | // MimeTypePresentation mime type validation for content type field 36 | MimeTypePresentation = "presentation" 37 | 38 | // MimeTypeSpreadSheet mime type validation for content type field 39 | MimeTypeSpreadSheet = "spreadsheet" 40 | 41 | // MimeTypePDF mime type validation for content type field 42 | MimeTypePDF = "pdfdocument" 43 | 44 | // MimeTypeArchive mime type validation for content type field 45 | MimeTypeArchive = "archive" 46 | 47 | // MimeTypeCode mime type validation for content type field 48 | MimeTypeCode = "code" 49 | 50 | // MimeTypeMarkup mime type validation for content type field 51 | MimeTypeMarkup = "markup" 52 | ) 53 | 54 | // FieldValidationMimeType model 55 | type FieldValidationMimeType struct { 56 | MimeTypes []string `json:"linkMimetypeGroup,omitempty"` 57 | } 58 | 59 | // MinMax model 60 | type MinMax struct { 61 | Min float64 `json:"min,omitempty"` 62 | Max float64 `json:"max,omitempty"` 63 | } 64 | 65 | // DateMinMax model 66 | type DateMinMax struct { 67 | Min time.Time `json:"min,omitempty"` 68 | Max time.Time `json:"max,omitempty"` 69 | } 70 | 71 | // FieldValidationDimension model 72 | type FieldValidationDimension struct { 73 | Width *MinMax `json:"width,omitempty"` 74 | Height *MinMax `json:"height,omitempty"` 75 | ErrorMessage string `json:"message,omitempty"` 76 | } 77 | 78 | // MarshalJSON for custom json marshaling 79 | func (v *FieldValidationDimension) MarshalJSON() ([]byte, error) { 80 | type dimension struct { 81 | Width *MinMax `json:"width,omitempty"` 82 | Height *MinMax `json:"height,omitempty"` 83 | } 84 | 85 | return json.Marshal(&struct { 86 | AssetImageDimensions *dimension `json:"assetImageDimensions,omitempty"` 87 | Message string `json:"message,omitempty"` 88 | }{ 89 | AssetImageDimensions: &dimension{ 90 | Width: v.Width, 91 | Height: v.Height, 92 | }, 93 | Message: v.ErrorMessage, 94 | }) 95 | } 96 | 97 | // UnmarshalJSON for custom json unmarshaling 98 | func (v *FieldValidationDimension) UnmarshalJSON(data []byte) error { 99 | payload := map[string]interface{}{} 100 | if err := json.Unmarshal(data, &payload); err != nil { 101 | return err 102 | } 103 | 104 | dimensionData := payload["assetImageDimensions"].(map[string]interface{}) 105 | 106 | if width, ok := dimensionData["width"].(map[string]interface{}); ok { 107 | v.Width = &MinMax{} 108 | 109 | if min, ok := width["min"].(float64); ok { 110 | v.Width.Min = min 111 | } 112 | 113 | if max, ok := width["min"].(float64); ok { 114 | v.Width.Max = max 115 | } 116 | } 117 | 118 | if height, ok := dimensionData["height"].(map[string]interface{}); ok { 119 | v.Height = &MinMax{} 120 | 121 | if min, ok := height["min"].(float64); ok { 122 | v.Height.Min = min 123 | } 124 | 125 | if max, ok := height["max"].(float64); ok { 126 | v.Height.Max = max 127 | } 128 | } 129 | 130 | if val, ok := payload["message"].(string); ok { 131 | v.ErrorMessage = val 132 | } 133 | 134 | return nil 135 | } 136 | 137 | // FieldValidationFileSize model 138 | type FieldValidationFileSize struct { 139 | Size *MinMax `json:"assetFileSize,omitempty"` 140 | ErrorMessage string `json:"message,omitempty"` 141 | } 142 | 143 | // FieldValidationUnique model 144 | type FieldValidationUnique struct { 145 | Unique bool `json:"unique"` 146 | } 147 | 148 | // FieldValidationPredefinedValues model 149 | type FieldValidationPredefinedValues struct { 150 | In []interface{} `json:"in,omitempty"` 151 | ErrorMessage string `json:"message"` 152 | } 153 | 154 | // FieldValidationRange model 155 | type FieldValidationRange struct { 156 | Range *MinMax `json:"range,omitempty"` 157 | ErrorMessage string `json:"message,omitempty"` 158 | } 159 | 160 | // FieldValidationDate model 161 | type FieldValidationDate struct { 162 | Range *DateMinMax `json:"dateRange,omitempty"` 163 | ErrorMessage string `json:"message,omitempty"` 164 | } 165 | 166 | // MarshalJSON for custom json marshaling 167 | func (v *FieldValidationDate) MarshalJSON() ([]byte, error) { 168 | type dateRange struct { 169 | Min string `json:"min,omitempty"` 170 | Max string `json:"max,omitempty"` 171 | } 172 | 173 | return json.Marshal(&struct { 174 | DateRange *dateRange `json:"dateRange,omitempty"` 175 | Message string `json:"message,omitempty"` 176 | }{ 177 | DateRange: &dateRange{ 178 | Min: v.Range.Max.Format("2006-01-02T03:04:05"), 179 | Max: v.Range.Max.Format("2006-01-02T03:04:05"), 180 | }, 181 | Message: v.ErrorMessage, 182 | }) 183 | } 184 | 185 | // UnmarshalJSON for custom json unmarshaling 186 | func (v *FieldValidationDate) UnmarshalJSON(data []byte) error { 187 | payload := map[string]interface{}{} 188 | if err := json.Unmarshal(data, &payload); err != nil { 189 | return err 190 | } 191 | 192 | dateRangeData := payload["dateRange"].(map[string]interface{}) 193 | 194 | v.Range = &DateMinMax{} 195 | 196 | if min, ok := dateRangeData["min"].(string); ok { 197 | minDate, err := time.Parse("2006-01-02T03:04:05", min) 198 | if err != nil { 199 | return err 200 | } 201 | 202 | v.Range.Min = minDate 203 | } 204 | 205 | if max, ok := dateRangeData["max"].(string); ok { 206 | maxDate, err := time.Parse("2006-01-02T03:04:05", max) 207 | if err != nil { 208 | return err 209 | } 210 | 211 | v.Range.Max = maxDate 212 | } 213 | 214 | if val, ok := payload["message"].(string); ok { 215 | v.ErrorMessage = val 216 | } 217 | 218 | return nil 219 | } 220 | 221 | // FieldValidationSize model 222 | type FieldValidationSize struct { 223 | Size *MinMax `json:"size,omitempty"` 224 | ErrorMessage string `json:"message,omitempty"` 225 | } 226 | 227 | const ( 228 | // FieldValidationRegexPatternEmail email validation 229 | FieldValidationRegexPatternEmail = `^\w[\w.-]*@([\w-]+\.)+[\w-]+$` 230 | 231 | // FieldValidationRegexPatternURL url validation 232 | FieldValidationRegexPatternURL = `^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$` 233 | 234 | // FieldValidationRegexPatternUSDate us date validation 235 | FieldValidationRegexPatternUSDate = `^(0?[1-9]|[12][0-9]|3[01])[- \/.](0?[1-9]|1[012])[- \/.](19|20)?\d\d$` 236 | 237 | // FieldValidationRegexPatternEuorpeanDate euorpean date validation 238 | FieldValidationRegexPatternEuorpeanDate = `^(0?[1-9]|[12][0-9]|3[01])[- \/.](0?[1-9]|1[012])[- \/.](19|20)?\d\d$` 239 | 240 | // FieldValidationRegexPattern12HourTime 12-hour time validation 241 | FieldValidationRegexPattern12HourTime = `^(0?[1-9]|1[012]):[0-5][0-9](:[0-5][0-9])?\s*[aApP][mM]$` 242 | 243 | // FieldValidationRegexPattern24HourTime 24-hour time validation 244 | FieldValidationRegexPattern24HourTime = `^(0?[0-9]|1[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$` 245 | 246 | // FieldValidationRegexPatternUSPhoneNumber us phone number validation 247 | FieldValidationRegexPatternUSPhoneNumber = `^\d[ -.]?\(?\d\d\d\)?[ -.]?\d\d\d[ -.]?\d\d\d\d$` 248 | 249 | // FieldValidationRegexPatternUSZipCode us zip code validation 250 | FieldValidationRegexPatternUSZipCode = `^\d{5}$|^\d{5}-\d{4}$}` 251 | ) 252 | 253 | // Regex model 254 | type Regex struct { 255 | Pattern string `json:"pattern,omitempty"` 256 | Flags string `json:"flags,omitempty"` 257 | } 258 | 259 | // FieldValidationRegex model 260 | type FieldValidationRegex struct { 261 | Regex *Regex `json:"regexp,omitempty"` 262 | ErrorMessage string `json:"message,omitempty"` 263 | } 264 | -------------------------------------------------------------------------------- /content_type_field_validations_test.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestFieldValidationLink(t *testing.T) { 13 | var err error 14 | assert := assert.New(t) 15 | 16 | validation := &FieldValidationLink{ 17 | LinkContentType: []string{"test", "test2"}, 18 | } 19 | 20 | data, err := json.Marshal(validation) 21 | assert.Nil(err) 22 | assert.Equal("{\"linkContentType\":[\"test\",\"test2\"]}", string(data)) 23 | } 24 | 25 | func TestFieldValidationUnique(t *testing.T) { 26 | var err error 27 | assert := assert.New(t) 28 | 29 | validation := &FieldValidationUnique{ 30 | Unique: false, 31 | } 32 | 33 | data, err := json.Marshal(validation) 34 | assert.Nil(err) 35 | assert.Equal("{\"unique\":false}", string(data)) 36 | } 37 | 38 | func TestFieldValidationPredefinedValues(t *testing.T) { 39 | var err error 40 | assert := assert.New(t) 41 | 42 | validation := &FieldValidationPredefinedValues{ 43 | In: []interface{}{5, 10, "string", 6.4}, 44 | ErrorMessage: "error message", 45 | } 46 | 47 | data, err := json.Marshal(validation) 48 | assert.Nil(err) 49 | assert.Equal("{\"in\":[5,10,\"string\",6.4],\"message\":\"error message\"}", string(data)) 50 | } 51 | 52 | func TestFieldValidationRange(t *testing.T) { 53 | var err error 54 | assert := assert.New(t) 55 | 56 | // between 57 | validation := &FieldValidationRange{ 58 | Range: &MinMax{ 59 | Min: 60, 60 | Max: 100, 61 | }, 62 | ErrorMessage: "error message", 63 | } 64 | data, err := json.Marshal(validation) 65 | assert.Nil(err) 66 | assert.Equal("{\"range\":{\"min\":60,\"max\":100},\"message\":\"error message\"}", string(data)) 67 | 68 | var validationCheck FieldValidationRange 69 | err = json.NewDecoder(bytes.NewReader(data)).Decode(&validationCheck) 70 | assert.Nil(err) 71 | assert.Equal(float64(60), validationCheck.Range.Min) 72 | assert.Equal(float64(100), validationCheck.Range.Max) 73 | assert.Equal("error message", validationCheck.ErrorMessage) 74 | 75 | // greater than equal to 76 | validation = &FieldValidationRange{ 77 | Range: &MinMax{ 78 | Min: 10, 79 | }, 80 | ErrorMessage: "error message", 81 | } 82 | data, err = json.Marshal(validation) 83 | assert.Nil(err) 84 | assert.Equal("{\"range\":{\"min\":10},\"message\":\"error message\"}", string(data)) 85 | validationCheck = FieldValidationRange{} 86 | err = json.NewDecoder(bytes.NewReader(data)).Decode(&validationCheck) 87 | assert.Nil(err) 88 | assert.Equal(float64(10), validationCheck.Range.Min) 89 | assert.Equal(float64(0), validationCheck.Range.Max) 90 | assert.Equal("error message", validationCheck.ErrorMessage) 91 | 92 | // less than equal to 93 | validation = &FieldValidationRange{ 94 | Range: &MinMax{ 95 | Max: 90, 96 | }, 97 | ErrorMessage: "error message", 98 | } 99 | data, err = json.Marshal(validation) 100 | assert.Nil(err) 101 | assert.Equal("{\"range\":{\"max\":90},\"message\":\"error message\"}", string(data)) 102 | validationCheck = FieldValidationRange{} 103 | err = json.NewDecoder(bytes.NewReader(data)).Decode(&validationCheck) 104 | assert.Nil(err) 105 | assert.Equal(float64(90), validationCheck.Range.Max) 106 | assert.Equal(float64(0), validationCheck.Range.Min) 107 | assert.Equal("error message", validationCheck.ErrorMessage) 108 | } 109 | 110 | func TestFieldValidationSize(t *testing.T) { 111 | var err error 112 | assert := assert.New(t) 113 | 114 | // between 115 | validation := &FieldValidationSize{ 116 | Size: &MinMax{ 117 | Min: 4, 118 | Max: 6, 119 | }, 120 | ErrorMessage: "error message", 121 | } 122 | data, err := json.Marshal(validation) 123 | assert.Nil(err) 124 | assert.Equal("{\"size\":{\"min\":4,\"max\":6},\"message\":\"error message\"}", string(data)) 125 | 126 | var validationCheck FieldValidationSize 127 | err = json.NewDecoder(bytes.NewReader(data)).Decode(&validationCheck) 128 | assert.Nil(err) 129 | assert.Equal(float64(4), validationCheck.Size.Min) 130 | assert.Equal(float64(6), validationCheck.Size.Max) 131 | assert.Equal("error message", validationCheck.ErrorMessage) 132 | } 133 | 134 | func TestFieldValidationDate(t *testing.T) { 135 | var err error 136 | assert := assert.New(t) 137 | 138 | layout := "2006-01-02T03:04:05" 139 | min := time.Now() 140 | max := time.Now() 141 | 142 | minStr := min.Format(layout) 143 | maxStr := max.Format(layout) 144 | 145 | validation := &FieldValidationDate{ 146 | Range: &DateMinMax{ 147 | Min: min, 148 | Max: max, 149 | }, 150 | ErrorMessage: "error message", 151 | } 152 | data, err := json.Marshal(validation) 153 | assert.Nil(err) 154 | assert.Equal("{\"dateRange\":{\"min\":\""+minStr+"\",\"max\":\""+maxStr+"\"},\"message\":\"error message\"}", string(data)) 155 | 156 | var validationCheck FieldValidationDate 157 | err = json.NewDecoder(bytes.NewReader(data)).Decode(&validationCheck) 158 | assert.Nil(err) 159 | assert.Equal(minStr, validationCheck.Range.Min.Format(layout)) 160 | assert.Equal(maxStr, validationCheck.Range.Max.Format(layout)) 161 | assert.Equal("error message", validationCheck.ErrorMessage) 162 | } 163 | -------------------------------------------------------------------------------- /contentful.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "net/http/httputil" 10 | "net/url" 11 | "strconv" 12 | "time" 13 | 14 | "moul.io/http2curl" 15 | ) 16 | 17 | // Client model 18 | type Client struct { 19 | client *http.Client 20 | api string 21 | token string 22 | Debug bool 23 | QueryParams map[string]string 24 | Headers map[string]string 25 | BaseURL string 26 | Environment string 27 | commonService service 28 | 29 | Spaces *SpacesService 30 | APIKeys *APIKeyService 31 | Assets *AssetsService 32 | ContentTypes *ContentTypesService 33 | Entries *EntriesService 34 | Locales *LocalesService 35 | Webhooks *WebhooksService 36 | } 37 | 38 | type service struct { 39 | c *Client 40 | } 41 | 42 | // NewCMA returns a CMA client 43 | func NewCMA(token string) *Client { 44 | c := &Client{ 45 | client: http.DefaultClient, 46 | api: "CMA", 47 | token: token, 48 | Debug: false, 49 | Headers: map[string]string{ 50 | "Authorization": fmt.Sprintf("Bearer %s", token), 51 | "Content-Type": "application/vnd.contentful.management.v1+json", 52 | "X-Contentful-User-Agent": fmt.Sprintf("sdk contentful-go/%s", Version), 53 | }, 54 | BaseURL: "https://api.contentful.com", 55 | Environment: "master", 56 | } 57 | c.commonService.c = c 58 | 59 | c.Spaces = (*SpacesService)(&c.commonService) 60 | c.APIKeys = (*APIKeyService)(&c.commonService) 61 | c.Assets = (*AssetsService)(&c.commonService) 62 | c.ContentTypes = (*ContentTypesService)(&c.commonService) 63 | c.Entries = (*EntriesService)(&c.commonService) 64 | c.Locales = (*LocalesService)(&c.commonService) 65 | c.Webhooks = (*WebhooksService)(&c.commonService) 66 | return c 67 | } 68 | 69 | // NewCDA returns a CDA client 70 | func NewCDA(token string) *Client { 71 | c := &Client{ 72 | client: http.DefaultClient, 73 | api: "CDA", 74 | token: token, 75 | Debug: false, 76 | Headers: map[string]string{ 77 | "Authorization": "Bearer " + token, 78 | "Content-Type": "application/vnd.contentful.delivery.v1+json", 79 | "X-Contentful-User-Agent": fmt.Sprintf("sdk contentful-go/%s", Version), 80 | }, 81 | BaseURL: "https://cdn.contentful.com", 82 | Environment: "master", 83 | } 84 | c.commonService.c = c 85 | 86 | c.Spaces = (*SpacesService)(&c.commonService) 87 | c.APIKeys = (*APIKeyService)(&c.commonService) 88 | c.Assets = (*AssetsService)(&c.commonService) 89 | c.ContentTypes = (*ContentTypesService)(&c.commonService) 90 | c.Entries = (*EntriesService)(&c.commonService) 91 | c.Locales = (*LocalesService)(&c.commonService) 92 | c.Webhooks = (*WebhooksService)(&c.commonService) 93 | 94 | return c 95 | } 96 | 97 | // NewCPA returns a CPA client 98 | func NewCPA(token string) *Client { 99 | c := &Client{ 100 | client: http.DefaultClient, 101 | Debug: false, 102 | api: "CPA", 103 | token: token, 104 | Headers: map[string]string{ 105 | "Authorization": "Bearer " + token, 106 | }, 107 | BaseURL: "https://preview.contentful.com", 108 | } 109 | 110 | c.Spaces = &SpacesService{c: c} 111 | c.APIKeys = &APIKeyService{c: c} 112 | c.Assets = &AssetsService{c: c} 113 | c.ContentTypes = &ContentTypesService{c: c} 114 | c.Entries = &EntriesService{c: c} 115 | c.Locales = &LocalesService{c: c} 116 | c.Webhooks = &WebhooksService{c: c} 117 | 118 | return c 119 | } 120 | 121 | // SetOrganization sets the given organization id 122 | func (c *Client) SetOrganization(organizationID string) *Client { 123 | c.Headers["X-Contentful-Organization"] = organizationID 124 | 125 | return c 126 | } 127 | 128 | // SetEnvironment sets the given environment. 129 | // https://www.contentful.com/developers/docs/references/content-management-api/#/reference/environments 130 | func (c *Client) SetEnvironment(environment string) *Client { 131 | c.Environment = environment 132 | return c 133 | } 134 | 135 | // SetHTTPClient sets the underlying http.Client used to make requests. 136 | func (c *Client) SetHTTPClient(client *http.Client) { 137 | c.client = client 138 | } 139 | 140 | func (c *Client) newRequest(method, path string, query url.Values, body io.Reader) (*http.Request, error) { 141 | u, err := url.Parse(c.BaseURL) 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | // set query params 147 | for key, value := range c.QueryParams { 148 | query.Set(key, value) 149 | } 150 | 151 | u.Path = path 152 | u.RawQuery = query.Encode() 153 | 154 | req, err := http.NewRequest(method, u.String(), body) 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | // set headers 160 | for key, value := range c.Headers { 161 | req.Header.Set(key, value) 162 | } 163 | 164 | return req, nil 165 | } 166 | 167 | func (c *Client) do(req *http.Request, v interface{}) error { 168 | if c.Debug == true { 169 | command, _ := http2curl.GetCurlCommand(req) 170 | fmt.Println(command) 171 | } 172 | 173 | res, err := c.client.Do(req) 174 | if err != nil { 175 | return err 176 | } 177 | 178 | if res.StatusCode >= 200 && res.StatusCode < 400 { 179 | if v != nil { 180 | defer res.Body.Close() 181 | err = json.NewDecoder(res.Body).Decode(v) 182 | if err != nil { 183 | return err 184 | } 185 | } 186 | 187 | return nil 188 | } 189 | 190 | // parse api response 191 | apiError := c.handleError(req, res) 192 | 193 | // return apiError if it is not rate limit error 194 | if _, ok := apiError.(RateLimitExceededError); !ok { 195 | return apiError 196 | } 197 | 198 | resetHeader := res.Header.Get("x-contentful-ratelimit-reset") 199 | 200 | // return apiError if Ratelimit-Reset header is not presented 201 | if resetHeader == "" { 202 | return apiError 203 | } 204 | 205 | // wait X-Contentful-Ratelimit-Reset amount of seconds 206 | waitSeconds, err := strconv.Atoi(resetHeader) 207 | if err != nil { 208 | return apiError 209 | } 210 | 211 | time.Sleep(time.Second * time.Duration(waitSeconds)) 212 | 213 | return c.do(req, v) 214 | } 215 | 216 | func (c *Client) handleError(req *http.Request, res *http.Response) error { 217 | if c.Debug == true { 218 | dump, err := httputil.DumpResponse(res, true) 219 | if err != nil { 220 | log.Fatal(err) 221 | } 222 | 223 | fmt.Printf("%q", dump) 224 | } 225 | 226 | var e ErrorResponse 227 | defer res.Body.Close() 228 | err := json.NewDecoder(res.Body).Decode(&e) 229 | if err != nil { 230 | return err 231 | } 232 | 233 | apiError := APIError{ 234 | req: req, 235 | res: res, 236 | err: &e, 237 | } 238 | 239 | switch errType := e.Sys.ID; errType { 240 | case "NotFound": 241 | return NotFoundError{apiError} 242 | case "RateLimitExceeded": 243 | return RateLimitExceededError{apiError} 244 | case "AccessTokenInvalid": 245 | return AccessTokenInvalidError{apiError} 246 | case "ValidationFailed": 247 | return ValidationFailedError{apiError} 248 | case "VersionMismatch": 249 | return VersionMismatchError{apiError} 250 | case "Conflict": 251 | return VersionMismatchError{apiError} 252 | default: 253 | return e 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /contentful_test.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "strconv" 13 | "strings" 14 | "testing" 15 | "time" 16 | 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | var ( 21 | server *httptest.Server 22 | cma *Client 23 | c *Client 24 | CMAToken = "b4c0n73n7fu1" 25 | CDAToken = "cda-token" 26 | CPAToken = "cpa-token" 27 | spaceID = "id1" 28 | organizationID = "org-id" 29 | ) 30 | 31 | func readTestData(fileName string) string { 32 | path := "testdata/" + fileName 33 | content, err := ioutil.ReadFile(path) 34 | if err != nil { 35 | log.Fatal(err) 36 | return "" 37 | } 38 | 39 | return string(content) 40 | } 41 | 42 | func checkHeaders(req *http.Request, assert *assert.Assertions) { 43 | assert.Equal("Bearer "+CMAToken, req.Header.Get("Authorization")) 44 | assert.Equal("application/vnd.contentful.management.v1+json", req.Header.Get("Content-Type")) 45 | } 46 | 47 | func spaceFromTestData(fileName string) (*Space, error) { 48 | content := readTestData(fileName) 49 | 50 | var space Space 51 | err := json.NewDecoder(strings.NewReader(content)).Decode(&space) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return &space, nil 57 | } 58 | 59 | func webhookFromTestData(fileName string) (*Webhook, error) { 60 | content := readTestData(fileName) 61 | 62 | var webhook Webhook 63 | err := json.NewDecoder(strings.NewReader(content)).Decode(&webhook) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | return &webhook, nil 69 | } 70 | 71 | func contentTypeFromTestData(fileName string) (*ContentType, error) { 72 | content := readTestData(fileName) 73 | 74 | var ct ContentType 75 | err := json.NewDecoder(strings.NewReader(content)).Decode(&ct) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | return &ct, nil 81 | } 82 | 83 | func localeFromTestData(fileName string) (*Locale, error) { 84 | content := readTestData(fileName) 85 | 86 | var locale Locale 87 | err := json.NewDecoder(strings.NewReader(content)).Decode(&locale) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | return &locale, nil 93 | } 94 | 95 | func setup() { 96 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 97 | fixture := strings.Replace(r.URL.Path, "/", "-", -1) 98 | fixture = strings.TrimLeft(fixture, "-") 99 | var path string 100 | 101 | if e := r.URL.Query().Get("error"); e != "" { 102 | path = "testdata/error-" + e + ".json" 103 | } else { 104 | if r.Method == "GET" { 105 | path = "testdata/" + fixture + ".json" 106 | } 107 | 108 | if r.Method == "POST" { 109 | path = "testdata/" + fixture + "-new.json" 110 | } 111 | 112 | if r.Method == "PUT" { 113 | path = "testdata/" + fixture + "-updated.json" 114 | } 115 | } 116 | 117 | file, err := ioutil.ReadFile(path) 118 | if err != nil { 119 | fmt.Fprintln(w, err) 120 | return 121 | } 122 | 123 | fmt.Fprintln(w, string(file)) 124 | return 125 | }) 126 | 127 | server = httptest.NewServer(handler) 128 | 129 | c = NewCMA(CMAToken) 130 | c.BaseURL = server.URL 131 | } 132 | 133 | func teardown() { 134 | server.Close() 135 | c = nil 136 | } 137 | 138 | func TestContentfulNewCMA(t *testing.T) { 139 | assert := assert.New(t) 140 | 141 | cma := NewCMA(CMAToken) 142 | assert.IsType(Client{}, *cma) 143 | assert.Equal("https://api.contentful.com", cma.BaseURL) 144 | assert.Equal("CMA", cma.api) 145 | assert.Equal(CMAToken, cma.token) 146 | assert.Equal(fmt.Sprintf("Bearer %s", CMAToken), cma.Headers["Authorization"]) 147 | assert.Equal("application/vnd.contentful.management.v1+json", cma.Headers["Content-Type"]) 148 | assert.Equal(fmt.Sprintf("sdk contentful-go/%s", Version), cma.Headers["X-Contentful-User-Agent"]) 149 | } 150 | 151 | func TestContentfulNewCDA(t *testing.T) { 152 | assert := assert.New(t) 153 | 154 | cda := NewCDA(CDAToken) 155 | assert.IsType(Client{}, *cda) 156 | assert.Equal("https://cdn.contentful.com", cda.BaseURL) 157 | assert.Equal("CDA", cda.api) 158 | assert.Equal(CDAToken, cda.token) 159 | assert.Equal(fmt.Sprintf("Bearer %s", CDAToken), cda.Headers["Authorization"]) 160 | assert.Equal("application/vnd.contentful.delivery.v1+json", cda.Headers["Content-Type"]) 161 | assert.Equal(fmt.Sprintf("sdk contentful-go/%s", Version), cda.Headers["X-Contentful-User-Agent"]) 162 | } 163 | 164 | func TestContentfulNewCPA(t *testing.T) { 165 | assert := assert.New(t) 166 | 167 | cpa := NewCPA(CPAToken) 168 | assert.IsType(Client{}, *cpa) 169 | assert.Equal("https://preview.contentful.com", cpa.BaseURL) 170 | assert.Equal("CPA", cpa.api) 171 | assert.Equal(CPAToken, cpa.token) 172 | } 173 | 174 | func TestContentfulSetOrganization(t *testing.T) { 175 | assert := assert.New(t) 176 | 177 | cma := NewCMA(CMAToken) 178 | cma.SetOrganization(organizationID) 179 | assert.Equal(organizationID, cma.Headers["X-Contentful-Organization"]) 180 | } 181 | 182 | func TestContentfulSetClient(t *testing.T) { 183 | assert := assert.New(t) 184 | 185 | newClient := &http.Client{} 186 | cma := NewCMA(CMAToken) 187 | cma.SetHTTPClient(newClient) 188 | assert.Equal(newClient, cma.client) 189 | } 190 | 191 | func TestNewRequest(t *testing.T) { 192 | setup() 193 | defer teardown() 194 | 195 | assert := assert.New(t) 196 | 197 | method := "GET" 198 | path := "/some/path" 199 | query := url.Values{} 200 | query.Add("foo", "bar") 201 | query.Add("faz", "zoo") 202 | 203 | expectedURL, _ := url.Parse(c.BaseURL) 204 | expectedURL.Path = path 205 | expectedURL.RawQuery = query.Encode() 206 | 207 | req, err := c.newRequest(method, path, query, nil) 208 | assert.Nil(err) 209 | assert.Equal(req.Header.Get("Authorization"), "Bearer "+CMAToken) 210 | assert.Equal(req.Header.Get("Content-Type"), "application/vnd.contentful.management.v1+json") 211 | assert.Equal(req.Method, method) 212 | assert.Equal(req.URL.String(), expectedURL.String()) 213 | 214 | method = "POST" 215 | type RequestBody struct { 216 | Name string `json:"name"` 217 | Age int `json:"age"` 218 | } 219 | bodyData := RequestBody{ 220 | Name: "test", 221 | Age: 10, 222 | } 223 | body, _ := json.Marshal(bodyData) 224 | req, err = c.newRequest(method, path, query, bytes.NewReader(body)) 225 | assert.Nil(err) 226 | assert.Equal(req.Header.Get("Authorization"), "Bearer "+CMAToken) 227 | assert.Equal(req.Header.Get("Content-Type"), "application/vnd.contentful.management.v1+json") 228 | assert.Equal(req.Method, method) 229 | assert.Equal(req.URL.String(), expectedURL.String()) 230 | defer req.Body.Close() 231 | var requestBody RequestBody 232 | err = json.NewDecoder(req.Body).Decode(&requestBody) 233 | assert.Nil(err) 234 | assert.Equal(requestBody, bodyData) 235 | } 236 | 237 | func TestHandleError(t *testing.T) { 238 | setup() 239 | defer teardown() 240 | 241 | assert := assert.New(t) 242 | 243 | method := "GET" 244 | path := "/some/path" 245 | requestID := "request-id" 246 | query := url.Values{} 247 | errResponse := ErrorResponse{ 248 | Sys: &Sys{ 249 | ID: "AccessTokenInvalid", 250 | Type: "Error", 251 | }, 252 | Message: "Access token is invalid", 253 | RequestID: requestID, 254 | } 255 | 256 | marshaled, _ := json.Marshal(errResponse) 257 | errResponseReader := bytes.NewReader(marshaled) 258 | errResponseReadCloser := ioutil.NopCloser(errResponseReader) 259 | 260 | req, _ := c.newRequest(method, path, query, nil) 261 | responseHeaders := http.Header{} 262 | responseHeaders.Add("X-Contentful-Request-Id", requestID) 263 | res := &http.Response{ 264 | Header: responseHeaders, 265 | StatusCode: http.StatusUnauthorized, 266 | Body: errResponseReadCloser, 267 | Request: req, 268 | } 269 | 270 | err := c.handleError(req, res) 271 | assert.IsType(AccessTokenInvalidError{}, err) 272 | assert.Equal(req, err.(AccessTokenInvalidError).APIError.req) 273 | assert.Equal(res, err.(AccessTokenInvalidError).APIError.res) 274 | assert.Equal(&errResponse, err.(AccessTokenInvalidError).APIError.err) 275 | } 276 | 277 | func TestBackoffForPerSecondLimiting(t *testing.T) { 278 | var err error 279 | assert := assert.New(t) 280 | rateLimited := true 281 | waitSeconds := 2 282 | 283 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 284 | if rateLimited == true { 285 | w.Header().Set("X-Contentful-Request-Id", "request-id") 286 | w.Header().Set("Content-Type", "application/vnd.contentful.management.v1+json") 287 | w.Header().Set("X-Contentful-Ratelimit-Hour-Limit", "36000") 288 | w.Header().Set("X-Contentful-Ratelimit-Hour-Remaining", "35883") 289 | w.Header().Set("X-Contentful-Ratelimit-Reset", strconv.Itoa(waitSeconds)) 290 | w.Header().Set("X-Contentful-Ratelimit-Second-Limit", "10") 291 | w.Header().Set("X-Contentful-Ratelimit-Second-Remaining", "0") 292 | w.WriteHeader(429) 293 | 294 | w.Write([]byte(readTestData("error-ratelimit.json"))) 295 | } else { 296 | w.Write([]byte(readTestData("space-1.json"))) 297 | } 298 | }) 299 | 300 | // test server 301 | server := httptest.NewServer(handler) 302 | defer server.Close() 303 | 304 | // cma client 305 | cma = NewCMA(CMAToken) 306 | cma.BaseURL = server.URL 307 | 308 | go func() { 309 | time.Sleep(time.Second * time.Duration(waitSeconds)) 310 | rateLimited = false 311 | }() 312 | 313 | space, err := cma.Spaces.Get("id1") 314 | assert.Nil(err) 315 | assert.Equal(space.Name, "Contentful Example API") 316 | assert.Equal(space.Sys.ID, "id1") 317 | } 318 | -------------------------------------------------------------------------------- /entry.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "strconv" 10 | ) 11 | 12 | // EntriesService service 13 | type EntriesService service 14 | 15 | //Entry model 16 | type Entry struct { 17 | locale string 18 | Sys *Sys `json:"sys"` 19 | Fields map[string]interface{} 20 | } 21 | 22 | // GetVersion returns entity version 23 | func (entry *Entry) GetVersion() int { 24 | version := 1 25 | if entry.Sys != nil { 26 | version = entry.Sys.Version 27 | } 28 | 29 | return version 30 | } 31 | 32 | // GetEntryKey returns the entry's keys 33 | func (service *EntriesService) GetEntryKey(entry *Entry, key string) (*EntryField, error) { 34 | ef := EntryField{ 35 | value: entry.Fields[key], 36 | } 37 | 38 | col, err := service.c.ContentTypes.List(entry.Sys.Space.Sys.ID).Next() 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | for _, ct := range col.ToContentType() { 44 | if ct.Sys.ID != entry.Sys.ContentType.Sys.ID { 45 | continue 46 | } 47 | 48 | for _, field := range ct.Fields { 49 | if field.ID != key { 50 | continue 51 | } 52 | 53 | ef.dataType = field.Type 54 | } 55 | } 56 | 57 | return &ef, nil 58 | } 59 | 60 | // List returns entries collection 61 | func (service *EntriesService) List(spaceID string) *Collection { 62 | path := fmt.Sprintf("/spaces/%s/environments/%s/entries", spaceID, service.c.Environment) 63 | 64 | req, err := service.c.newRequest(http.MethodGet, path, nil, nil) 65 | if err != nil { 66 | return &Collection{} 67 | } 68 | 69 | col := NewCollection(&CollectionOptions{}) 70 | col.c = service.c 71 | col.req = req 72 | 73 | return col 74 | } 75 | 76 | // Get returns a single entry 77 | func (service *EntriesService) Get(spaceID, entryID string) (*Entry, error) { 78 | path := fmt.Sprintf("/spaces/%s/entries/%s", spaceID, entryID) 79 | query := url.Values{} 80 | method := "GET" 81 | 82 | req, err := service.c.newRequest(method, path, query, nil) 83 | if err != nil { 84 | return &Entry{}, err 85 | } 86 | 87 | var entry Entry 88 | if ok := service.c.do(req, &entry); ok != nil { 89 | return nil, err 90 | } 91 | 92 | return &entry, err 93 | } 94 | 95 | // Upsert updates or creates a new entry 96 | func (service *EntriesService) Upsert(spaceID string, entry *Entry) error { 97 | fields := map[string]interface{}{ 98 | "fields": entry.Fields, 99 | } 100 | 101 | bytesArray, err := json.Marshal(fields) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | // Creating/updating an entry requires a content type to be provided 107 | if entry.Sys.ContentType == nil { 108 | return fmt.Errorf("creating/updating an entry requires a content type") 109 | } 110 | 111 | var path string 112 | var method string 113 | 114 | if entry.Sys != nil && entry.Sys.CreatedAt != "" { 115 | path = fmt.Sprintf("/spaces/%s/environments/%s/entries/%s", spaceID, service.c.Environment, entry.Sys.ID) 116 | method = http.MethodPut 117 | } else { 118 | path = fmt.Sprintf("/spaces/%s/environments/%s/entries", spaceID, service.c.Environment) 119 | method = http.MethodPost 120 | } 121 | 122 | req, err := service.c.newRequest(method, path, nil, bytes.NewReader(bytesArray)) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | version := strconv.Itoa(entry.Sys.Version) 128 | req.Header.Set("X-Contentful-Version", version) 129 | req.Header.Set("X-Contentful-Content-Type", entry.Sys.ContentType.Sys.ID) 130 | 131 | return service.c.do(req, entry) 132 | } 133 | 134 | // Delete the entry 135 | func (service *EntriesService) Delete(spaceID string, entryID string) error { 136 | path := fmt.Sprintf("/spaces/%s/entries/%s", spaceID, entryID) 137 | method := "DELETE" 138 | 139 | req, err := service.c.newRequest(method, path, nil, nil) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | return service.c.do(req, nil) 145 | } 146 | 147 | // Publish the entry 148 | func (service *EntriesService) Publish(spaceID string, entry *Entry) error { 149 | path := fmt.Sprintf("/spaces/%s/entries/%s/published", spaceID, entry.Sys.ID) 150 | method := "PUT" 151 | 152 | req, err := service.c.newRequest(method, path, nil, nil) 153 | if err != nil { 154 | return err 155 | } 156 | 157 | version := strconv.Itoa(entry.Sys.Version) 158 | req.Header.Set("X-Contentful-Version", version) 159 | 160 | return service.c.do(req, nil) 161 | } 162 | 163 | // Unpublish the entry 164 | func (service *EntriesService) Unpublish(spaceID string, entry *Entry) error { 165 | path := fmt.Sprintf("/spaces/%s/entries/%s/published", spaceID, entry.Sys.ID) 166 | method := "DELETE" 167 | 168 | req, err := service.c.newRequest(method, path, nil, nil) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | version := strconv.Itoa(entry.Sys.Version) 174 | req.Header.Set("X-Contentful-Version", version) 175 | 176 | return service.c.do(req, nil) 177 | } 178 | -------------------------------------------------------------------------------- /entry_field.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import "reflect" 4 | 5 | // EntryField model 6 | type EntryField struct { 7 | value interface{} 8 | dataType string 9 | } 10 | 11 | // String converts interface to string 12 | func (ef *EntryField) String() string { 13 | return ef.value.(string) 14 | } 15 | 16 | //LString returns the given locale 17 | func (ef *EntryField) LString(locale string) string { 18 | m := ef.value.(map[string]interface{}) 19 | 20 | if val, ok := m[locale]; ok { 21 | return val.(string) 22 | } 23 | 24 | panic("no such a locale") 25 | } 26 | 27 | //Integer converts interface to integer 28 | func (ef *EntryField) Integer() int { 29 | return int(ef.value.(float64)) 30 | } 31 | 32 | //LInteger converts interface to integer 33 | func (ef *EntryField) LInteger(locale string) int { 34 | m := ef.value.(map[string]interface{}) 35 | 36 | if val, ok := m[locale]; ok { 37 | return int(val.(float64)) 38 | } 39 | 40 | panic("no such a locale") 41 | } 42 | 43 | //Array converts interface to slice 44 | func (ef *EntryField) Array() []string { 45 | res := []string{} 46 | 47 | switch reflect.TypeOf(ef.value).Kind() { 48 | case reflect.Slice: 49 | s := reflect.ValueOf(ef.value) 50 | 51 | for i := 0; i < s.Len(); i++ { 52 | res = append(res, s.Index(i).Interface().(string)) 53 | } 54 | } 55 | 56 | return res 57 | } 58 | 59 | //LArray converts interface to slice 60 | func (ef *EntryField) LArray(locale string) []string { 61 | m := ef.value.(map[string]interface{}) 62 | 63 | if val, ok := m[locale]; ok { 64 | res := []string{} 65 | 66 | switch reflect.TypeOf(val).Kind() { 67 | case reflect.Slice: 68 | s := reflect.ValueOf(val) 69 | 70 | for i := 0; i < s.Len(); i++ { 71 | res = append(res, s.Index(i).Interface().(string)) 72 | } 73 | } 74 | 75 | return res 76 | } 77 | 78 | panic("no such a locale") 79 | } 80 | 81 | //LinkID returns link model 82 | func (ef *EntryField) LinkID() string { 83 | m := ef.value.(map[string]interface{}) 84 | sys := m["sys"].(map[string]interface{}) 85 | return sys["id"].(string) 86 | } 87 | 88 | //LLinkID returns link model 89 | func (ef *EntryField) LLinkID(locale string) string { 90 | m := ef.value.(map[string]interface{}) 91 | 92 | if val, ok := m[locale]; ok { 93 | m := val.(map[string]interface{}) 94 | sys := m["sys"].(map[string]interface{}) 95 | return sys["id"].(string) 96 | } 97 | 98 | panic("no such a locale") 99 | } 100 | 101 | //LinkType returns link model 102 | func (ef *EntryField) LinkType() string { 103 | m := ef.value.(map[string]interface{}) 104 | sys := m["sys"].(map[string]interface{}) 105 | return sys["linkType"].(string) 106 | } 107 | 108 | //LLinkType returns link model 109 | func (ef *EntryField) LLinkType(locale string) string { 110 | m := ef.value.(map[string]interface{}) 111 | 112 | if val, ok := m[locale]; ok { 113 | m := val.(map[string]interface{}) 114 | sys := m["sys"].(map[string]interface{}) 115 | return sys["linkType"].(string) 116 | } 117 | 118 | panic("no such a locale") 119 | } 120 | 121 | //Asset returns the linked asset 122 | func (ef *EntryField) Asset() *Asset { 123 | if ef.LinkType() != "Asset" { 124 | panic("you can only convert asset types") 125 | } 126 | 127 | // asset, _ := ef.space.GetAsset(ef.LinkID()) 128 | return &Asset{} 129 | } 130 | 131 | //LAsset returns the linked asset 132 | func (ef *EntryField) LAsset(locale string) *Asset { 133 | if ef.LLinkType(locale) != "Asset" { 134 | panic("you can only convert asset types") 135 | } 136 | 137 | // asset, _ := ef.space.GetAsset(ef.LLinkID(locale)) 138 | return &Asset{} 139 | } 140 | 141 | //Entry returns the linked entry 142 | func (ef *EntryField) Entry() *Entry { 143 | if ef.LinkType() != "Entry" { 144 | panic("you can only convert entry types") 145 | } 146 | 147 | // entry, _ := ef.space.GetEntries().Get(ef.LinkID()) 148 | return &Entry{} 149 | } 150 | 151 | //LEntry returns the linked entry 152 | func (ef *EntryField) LEntry(locale string) *Entry { 153 | if ef.LLinkType(locale) != "Entry" { 154 | panic("you can only convert entry types") 155 | } 156 | 157 | // entry, _ := ef.space.GetEntries().Get(ef.LLinkID(locale)) 158 | return &Entry{} 159 | } 160 | -------------------------------------------------------------------------------- /entry_test.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func ExampleEntriesService_Upsert_create() { 15 | cma := NewCMA("cma-token") 16 | 17 | entry := &Entry{ 18 | Sys: &Sys{ 19 | ID: "MyEntry", 20 | ContentType: &ContentType{ 21 | Sys: &Sys{ 22 | ID: "MyContentType", 23 | }, 24 | }, 25 | }, 26 | Fields: map[string]interface{}{ 27 | "Description": map[string]string{ 28 | "en-US": "Some example content...", 29 | }, 30 | }, 31 | } 32 | 33 | err := cma.Entries.Upsert("space-id", entry) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | } 38 | 39 | func ExampleEntryService_Upsert_update() { 40 | cma := NewCMA("cma-token") 41 | 42 | entry, err := cma.Entries.Get("space-id", "entry-id") 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | entry.Fields["Description"] = map[string]interface{}{ 48 | "en-US": "modified entry content", 49 | } 50 | 51 | err = cma.Entries.Upsert("space-id", entry) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | } 56 | 57 | func TestEntrySaveForCreate(t *testing.T) { 58 | var err error 59 | assert := assert.New(t) 60 | 61 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 62 | assert.Equal(r.Method, "POST") 63 | assert.Equal(r.RequestURI, "/spaces/"+spaceID+"/environments/master/entries") 64 | assert.Equal(r.Header["X-Contentful-Content-Type"], []string{"MyContentType"}) 65 | checkHeaders(r, assert) 66 | 67 | var payload map[string]interface{} 68 | err := json.NewDecoder(r.Body).Decode(&payload) 69 | assert.Nil(err) 70 | 71 | assert.NotNil(payload["fields"]) 72 | fields := payload["fields"].(map[string]interface{}) 73 | 74 | assert.Equal(fields["Description"], map[string]interface{}{"en-US": "Some test content..."}) 75 | 76 | w.WriteHeader(201) 77 | fmt.Fprintln(w, string(readTestData("entry_3.json"))) 78 | }) 79 | 80 | // test server 81 | server := httptest.NewServer(handler) 82 | defer server.Close() 83 | 84 | // cma client 85 | cma = NewCMA(CMAToken) 86 | cma.BaseURL = server.URL 87 | 88 | entry := &Entry{ 89 | Sys: &Sys{ 90 | ContentType: &ContentType{ 91 | Sys: &Sys{ 92 | ID: "MyContentType", 93 | }, 94 | }, 95 | }, 96 | Fields: map[string]interface{}{ 97 | "Description": map[string]string{ 98 | "en-US": "Some test content...", 99 | }, 100 | }, 101 | } 102 | 103 | err = cma.Entries.Upsert("id1", entry) 104 | assert.Nil(err) 105 | assert.Equal("foocat", entry.Sys.ID) 106 | } -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // ErrorResponse model 10 | type ErrorResponse struct { 11 | Sys *Sys `json:"sys"` 12 | Message string `json:"message,omitempty"` 13 | RequestID string `json:"requestId,omitempty"` 14 | Details *ErrorDetails `json:"details,omitempty"` 15 | } 16 | 17 | func (e ErrorResponse) Error() string { 18 | return e.Message 19 | } 20 | 21 | // ErrorDetails model 22 | type ErrorDetails struct { 23 | Errors []*ErrorDetail `json:"errors,omitempty"` 24 | } 25 | 26 | // ErrorDetail model 27 | type ErrorDetail struct { 28 | ID string `json:"id,omitempty"` 29 | Name string `json:"name,omitempty"` 30 | Path interface{} `json:"path,omitempty"` 31 | Details string `json:"details,omitempty"` 32 | Value interface{} `json:"value,omitempty"` 33 | } 34 | 35 | // APIError model 36 | type APIError struct { 37 | req *http.Request 38 | res *http.Response 39 | err *ErrorResponse 40 | } 41 | 42 | // AccessTokenInvalidError for 401 errors 43 | type AccessTokenInvalidError struct { 44 | APIError 45 | } 46 | 47 | func (e AccessTokenInvalidError) Error() string { 48 | return e.APIError.err.Message 49 | } 50 | 51 | // VersionMismatchError for 409 errors 52 | type VersionMismatchError struct { 53 | APIError 54 | } 55 | 56 | func (e VersionMismatchError) Error() string { 57 | return "Version " + e.APIError.req.Header.Get("X-Contentful-Version") + " is mismatched" 58 | } 59 | 60 | // ValidationFailedError model 61 | type ValidationFailedError struct { 62 | APIError 63 | } 64 | 65 | func (e ValidationFailedError) Error() string { 66 | msg := bytes.Buffer{} 67 | 68 | for _, err := range e.APIError.err.Details.Errors { 69 | if err.Name == "uniqueFieldIds" || err.Name == "uniqueFieldApiNames" { 70 | return msg.String() 71 | } 72 | msg.WriteString(fmt.Sprintf("%s\n", err.Details)) 73 | } 74 | 75 | return msg.String() 76 | } 77 | 78 | // NotFoundError for 404 errors 79 | type NotFoundError struct { 80 | APIError 81 | } 82 | 83 | func (e NotFoundError) Error() string { 84 | return "the requested resource can not be found" 85 | } 86 | 87 | // RateLimitExceededError for rate limit errors 88 | type RateLimitExceededError struct { 89 | APIError 90 | } 91 | 92 | func (e RateLimitExceededError) Error() string { 93 | return e.APIError.err.Message 94 | } 95 | 96 | // BadRequestError error model for bad request responses 97 | type BadRequestError struct{} 98 | 99 | // InvalidQueryError error model for invalid query responses 100 | type InvalidQueryError struct{} 101 | 102 | // AccessDeniedError error model for access denied responses 103 | type AccessDeniedError struct{} 104 | 105 | // ServerError error model for server error responses 106 | type ServerError struct{} 107 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNotFoundErrorResponse(t *testing.T) { 13 | var err error 14 | assert := assert.New(t) 15 | 16 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | w.WriteHeader(404) 18 | fmt.Fprintln(w, string(readTestData("error-notfound.json"))) 19 | }) 20 | 21 | // test server 22 | server := httptest.NewServer(handler) 23 | defer server.Close() 24 | 25 | // cma client 26 | cma = NewCMA(CMAToken) 27 | cma.BaseURL = server.URL 28 | 29 | // test space 30 | _, err = cma.Spaces.Get("unknown-space-id") 31 | assert.NotNil(err) 32 | _, ok := err.(NotFoundError) 33 | assert.Equal(true, ok) 34 | notFoundError := err.(NotFoundError) 35 | assert.Equal(404, notFoundError.APIError.res.StatusCode) 36 | assert.Equal("request-id", notFoundError.APIError.err.RequestID) 37 | assert.Equal("The resource could not be found.", notFoundError.APIError.err.Message) 38 | assert.Equal("Error", notFoundError.APIError.err.Sys.Type) 39 | assert.Equal("NotFound", notFoundError.APIError.err.Sys.ID) 40 | } 41 | 42 | func TestRateLimitExceededResponse(t *testing.T) { 43 | var err error 44 | assert := assert.New(t) 45 | 46 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 47 | w.WriteHeader(403) 48 | fmt.Fprintln(w, string(readTestData("error-ratelimit.json"))) 49 | }) 50 | 51 | // test server 52 | server := httptest.NewServer(handler) 53 | defer server.Close() 54 | 55 | // cma client 56 | cma = NewCMA(CMAToken) 57 | cma.BaseURL = server.URL 58 | 59 | // test space 60 | space := &Space{Name: "test-space"} 61 | err = cma.Spaces.Upsert(space) 62 | assert.NotNil(err) 63 | _, ok := err.(RateLimitExceededError) 64 | assert.Equal(true, ok) 65 | rateLimitExceededError := err.(RateLimitExceededError) 66 | assert.Equal(403, rateLimitExceededError.APIError.res.StatusCode) 67 | assert.Equal("request-id", rateLimitExceededError.APIError.err.RequestID) 68 | assert.Equal("You are creating too many Spaces.", rateLimitExceededError.APIError.err.Message) 69 | assert.Equal("Error", rateLimitExceededError.APIError.err.Sys.Type) 70 | assert.Equal("RateLimitExceeded", rateLimitExceededError.APIError.err.Sys.ID) 71 | } 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/contentful-labs/contentful-go 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.0 7 | github.com/pmezard/go-difflib v1.0.0 8 | github.com/stretchr/testify v1.1.4 9 | moul.io/http2curl v1.0.0 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.1.4 h1:ToftOQTytwshuOSj6bDSolVUa3GINfJP/fg3OkkOzQQ= 6 | github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 7 | moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8= 8 | moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= 9 | -------------------------------------------------------------------------------- /locale.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "strconv" 8 | ) 9 | 10 | // LocalesService service 11 | type LocalesService service 12 | 13 | // Locale model 14 | type Locale struct { 15 | Sys *Sys `json:"sys,omitempty"` 16 | 17 | // Locale name 18 | Name string `json:"name,omitempty"` 19 | 20 | // Language code 21 | Code string `json:"code,omitempty"` 22 | 23 | // If no content is provided for the locale, the Delivery API will return content in a locale specified below: 24 | FallbackCode string `json:"fallbackCode,omitempty"` 25 | 26 | // Make the locale as default locale for your account 27 | Default bool `json:"default,omitempty"` 28 | 29 | // Entries with required fields can still be published if locale is empty. 30 | Optional bool `json:"optional,omitempty"` 31 | 32 | // Includes locale in the Delivery API response. 33 | CDA bool `json:"contentDeliveryApi"` 34 | 35 | // Displays locale to editors and enables it in Management API. 36 | CMA bool `json:"contentManagementApi"` 37 | } 38 | 39 | // GetVersion returns entity version 40 | func (locale *Locale) GetVersion() int { 41 | version := 1 42 | if locale.Sys != nil { 43 | version = locale.Sys.Version 44 | } 45 | 46 | return version 47 | } 48 | 49 | // List returns a locales collection 50 | func (service *LocalesService) List(spaceID string) *Collection { 51 | path := fmt.Sprintf("/spaces/%s/locales", spaceID) 52 | method := "GET" 53 | 54 | req, err := service.c.newRequest(method, path, nil, nil) 55 | if err != nil { 56 | return &Collection{} 57 | } 58 | 59 | col := NewCollection(&CollectionOptions{}) 60 | col.c = service.c 61 | col.req = req 62 | 63 | return col 64 | } 65 | 66 | // Get returns a single locale entity 67 | func (service *LocalesService) Get(spaceID, localeID string) (*Locale, error) { 68 | path := fmt.Sprintf("/spaces/%s/locales/%s", spaceID, localeID) 69 | method := "GET" 70 | 71 | req, err := service.c.newRequest(method, path, nil, nil) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | var locale Locale 77 | if err := service.c.do(req, &locale); err != nil { 78 | return nil, err 79 | } 80 | 81 | return &locale, nil 82 | } 83 | 84 | // Delete the locale 85 | func (service *LocalesService) Delete(spaceID string, locale *Locale) error { 86 | path := fmt.Sprintf("/spaces/%s/locales/%s", spaceID, locale.Sys.ID) 87 | method := "DELETE" 88 | 89 | req, err := service.c.newRequest(method, path, nil, nil) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | version := strconv.Itoa(locale.Sys.Version) 95 | req.Header.Set("X-Contentful-Version", version) 96 | 97 | return service.c.do(req, nil) 98 | } 99 | 100 | // Upsert updates or creates a new locale entity 101 | func (service *LocalesService) Upsert(spaceID string, locale *Locale) error { 102 | bytesArray, err := json.Marshal(locale) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | var path string 108 | var method string 109 | 110 | if locale.Sys != nil && locale.Sys.CreatedAt != "" { 111 | path = fmt.Sprintf("/spaces/%s/locales/%s", spaceID, locale.Sys.ID) 112 | method = "PUT" 113 | } else { 114 | path = fmt.Sprintf("/spaces/%s/locales", spaceID) 115 | method = "POST" 116 | } 117 | 118 | req, err := service.c.newRequest(method, path, nil, bytes.NewReader(bytesArray)) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | req.Header.Set("X-Contentful-Version", strconv.Itoa(locale.GetVersion())) 124 | 125 | return service.c.do(req, locale) 126 | } 127 | -------------------------------------------------------------------------------- /locale_test.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestLocalesServiceList(t *testing.T) { 14 | var err error 15 | assert := assert.New(t) 16 | 17 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | assert.Equal(r.Method, "GET") 19 | assert.Equal(r.URL.Path, "/spaces/"+spaceID+"/locales") 20 | 21 | checkHeaders(r, assert) 22 | 23 | w.WriteHeader(200) 24 | fmt.Fprintln(w, readTestData("locales.json")) 25 | }) 26 | 27 | // test server 28 | server := httptest.NewServer(handler) 29 | defer server.Close() 30 | 31 | // cma client 32 | cma = NewCMA(CMAToken) 33 | cma.BaseURL = server.URL 34 | 35 | _, err = cma.Locales.List(spaceID).Next() 36 | assert.Nil(err) 37 | } 38 | 39 | func TestLocalesServiceGet(t *testing.T) { 40 | var err error 41 | assert := assert.New(t) 42 | 43 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 44 | assert.Equal(r.Method, "GET") 45 | assert.Equal(r.URL.Path, "/spaces/"+spaceID+"/locales/4aGeQYgByqQFJtToAOh2JJ") 46 | 47 | checkHeaders(r, assert) 48 | 49 | w.WriteHeader(200) 50 | fmt.Fprintln(w, readTestData("locale_1.json")) 51 | }) 52 | 53 | // test server 54 | server := httptest.NewServer(handler) 55 | defer server.Close() 56 | 57 | // cma client 58 | cma = NewCMA(CMAToken) 59 | cma.BaseURL = server.URL 60 | 61 | locale, err := cma.Locales.Get(spaceID, "4aGeQYgByqQFJtToAOh2JJ") 62 | assert.Nil(err) 63 | assert.Equal("U.S. English", locale.Name) 64 | assert.Equal("en-US", locale.Code) 65 | } 66 | 67 | func TestLocalesServiceUpsertCreate(t *testing.T) { 68 | var err error 69 | assert := assert.New(t) 70 | 71 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 72 | assert.Equal(r.Method, "POST") 73 | assert.Equal(r.RequestURI, "/spaces/"+spaceID+"/locales") 74 | 75 | checkHeaders(r, assert) 76 | 77 | var payload map[string]interface{} 78 | err := json.NewDecoder(r.Body).Decode(&payload) 79 | assert.Nil(err) 80 | assert.Equal("German (Austria)", payload["name"]) 81 | assert.Equal("de-AT", payload["code"]) 82 | 83 | w.WriteHeader(200) 84 | fmt.Fprintln(w, readTestData("locale_1.json")) 85 | }) 86 | 87 | // test server 88 | server := httptest.NewServer(handler) 89 | defer server.Close() 90 | 91 | // cma client 92 | cma = NewCMA(CMAToken) 93 | cma.BaseURL = server.URL 94 | 95 | locale := &Locale{ 96 | Name: "German (Austria)", 97 | Code: "de-AT", 98 | } 99 | 100 | err = cma.Locales.Upsert(spaceID, locale) 101 | assert.Nil(err) 102 | } 103 | 104 | func TestLocalesServiceUpsertUpdate(t *testing.T) { 105 | var err error 106 | assert := assert.New(t) 107 | 108 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 109 | assert.Equal(r.Method, "PUT") 110 | assert.Equal(r.RequestURI, "/spaces/"+spaceID+"/locales/4aGeQYgByqQFJtToAOh2JJ") 111 | 112 | checkHeaders(r, assert) 113 | 114 | var payload map[string]interface{} 115 | err := json.NewDecoder(r.Body).Decode(&payload) 116 | assert.Nil(err) 117 | assert.Equal("modified-name", payload["name"]) 118 | assert.Equal("modified-code", payload["code"]) 119 | 120 | w.WriteHeader(200) 121 | fmt.Fprintln(w, string(readTestData("locale_1.json"))) 122 | }) 123 | 124 | // test server 125 | server := httptest.NewServer(handler) 126 | defer server.Close() 127 | 128 | // cma client 129 | cma = NewCMA(CMAToken) 130 | cma.BaseURL = server.URL 131 | 132 | locale, err := localeFromTestData("locale_1.json") 133 | assert.Nil(err) 134 | 135 | locale.Name = "modified-name" 136 | locale.Code = "modified-code" 137 | 138 | err = cma.Locales.Upsert(spaceID, locale) 139 | assert.Nil(err) 140 | } 141 | 142 | func TestLocalesServiceDelete(t *testing.T) { 143 | var err error 144 | assert := assert.New(t) 145 | 146 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 147 | assert.Equal(r.Method, "DELETE") 148 | assert.Equal(r.RequestURI, "/spaces/"+spaceID+"/locales/4aGeQYgByqQFJtToAOh2JJ") 149 | checkHeaders(r, assert) 150 | 151 | w.WriteHeader(200) 152 | }) 153 | 154 | // test server 155 | server := httptest.NewServer(handler) 156 | defer server.Close() 157 | 158 | // cma client 159 | cma = NewCMA(CMAToken) 160 | cma.BaseURL = server.URL 161 | 162 | // test locale 163 | locale, err := localeFromTestData("locale_1.json") 164 | assert.Nil(err) 165 | 166 | // delete locale 167 | err = cma.Locales.Delete(spaceID, locale) 168 | assert.Nil(err) 169 | } 170 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "net/url" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | //Query model 12 | type Query struct { 13 | include uint16 14 | contentType string 15 | fields []string 16 | e map[string]interface{} 17 | ne map[string]interface{} 18 | all map[string][]string 19 | in map[string][]string 20 | nin map[string][]string 21 | exists []string 22 | notExists []string 23 | lt map[string]interface{} 24 | lte map[string]interface{} 25 | gt map[string]interface{} 26 | gte map[string]interface{} 27 | q string 28 | match map[string]string 29 | near map[string]string 30 | within map[string]string 31 | order []string 32 | limit uint16 33 | skip uint16 34 | mime string 35 | locale string 36 | } 37 | 38 | //NewQuery initilazies a new query 39 | func NewQuery() *Query { 40 | return &Query{ 41 | include: 0, 42 | contentType: "", 43 | fields: []string{}, 44 | e: make(map[string]interface{}), 45 | ne: make(map[string]interface{}), 46 | all: make(map[string][]string), 47 | in: make(map[string][]string), 48 | nin: make(map[string][]string), 49 | exists: []string{}, 50 | notExists: []string{}, 51 | lt: make(map[string]interface{}), 52 | lte: make(map[string]interface{}), 53 | gt: make(map[string]interface{}), 54 | gte: make(map[string]interface{}), 55 | q: "", 56 | match: make(map[string]string), 57 | near: make(map[string]string), 58 | within: make(map[string]string), 59 | order: []string{}, 60 | limit: 0, 61 | skip: 0, 62 | mime: "", 63 | locale: "", 64 | } 65 | } 66 | 67 | //Include query 68 | func (q *Query) Include(include uint16) *Query { 69 | q.include = include 70 | return q 71 | } 72 | 73 | //ContentType query 74 | func (q *Query) ContentType(ct string) *Query { 75 | q.contentType = ct 76 | return q 77 | } 78 | 79 | //Select query 80 | func (q *Query) Select(fields []string) *Query { 81 | q.fields = fields 82 | return q 83 | } 84 | 85 | //Equal equality query 86 | func (q *Query) Equal(field string, value interface{}) *Query { 87 | q.e[field] = value 88 | return q 89 | } 90 | 91 | //NotEqual [ne] query 92 | func (q *Query) NotEqual(field string, value interface{}) *Query { 93 | q.ne[field] = value 94 | return q 95 | } 96 | 97 | //All [all] query 98 | func (q *Query) All(field string, value []string) *Query { 99 | q.all[field] = value 100 | return q 101 | } 102 | 103 | //In [in] query 104 | func (q *Query) In(field string, value []string) *Query { 105 | q.in[field] = value 106 | return q 107 | } 108 | 109 | //NotIn [nin] query 110 | func (q *Query) NotIn(field string, value []string) *Query { 111 | q.nin[field] = value 112 | return q 113 | } 114 | 115 | //Exists [exists] query 116 | func (q *Query) Exists(field string) *Query { 117 | q.exists = append(q.exists, field) 118 | return q 119 | } 120 | 121 | //NotExists [exists] query 122 | func (q *Query) NotExists(field string) *Query { 123 | q.notExists = append(q.notExists, field) 124 | return q 125 | } 126 | 127 | //LessThan [lt] query 128 | func (q *Query) LessThan(field string, value interface{}) *Query { 129 | q.lt[field] = value 130 | return q 131 | } 132 | 133 | //LessThanOrEqual [lte] query 134 | func (q *Query) LessThanOrEqual(field string, value interface{}) *Query { 135 | q.lte[field] = value 136 | return q 137 | } 138 | 139 | //GreaterThan [gt] query 140 | func (q *Query) GreaterThan(field string, value interface{}) *Query { 141 | q.gt[field] = value 142 | return q 143 | } 144 | 145 | //GreaterThanOrEqual [lte] query 146 | func (q *Query) GreaterThanOrEqual(field string, value interface{}) *Query { 147 | q.gte[field] = value 148 | return q 149 | } 150 | 151 | //Query param 152 | func (q *Query) Query(qStr string) *Query { 153 | q.q = qStr 154 | return q 155 | } 156 | 157 | //Match param 158 | func (q *Query) Match(field, match string) *Query { 159 | q.match[field] = match 160 | return q 161 | } 162 | 163 | //Near param 164 | func (q *Query) Near(field string, lat, lon int16) *Query { 165 | q.near[field] = strconv.Itoa(int(lat)) + "," + strconv.Itoa(int(lon)) 166 | return q 167 | } 168 | 169 | //Within param 170 | func (q *Query) Within(field string, lat1, lon1, lat2, lon2 int16) *Query { 171 | q.within[field] = strconv.Itoa(int(lat1)) + "," + strconv.Itoa(int(lon1)) + "," + strconv.Itoa(int(lat2)) + "," + strconv.Itoa(int(lon2)) 172 | return q 173 | } 174 | 175 | //WithinRadius param 176 | func (q *Query) WithinRadius(field string, lat1, lon1, radius int16) *Query { 177 | q.within[field] = strconv.Itoa(int(lat1)) + "," + strconv.Itoa(int(lon1)) + "," + strconv.Itoa(int(radius)) 178 | return q 179 | } 180 | 181 | //Order param 182 | func (q *Query) Order(field string, reverse bool) *Query { 183 | if reverse { 184 | q.order = append(q.order, "-"+field) 185 | } else { 186 | q.order = append(q.order, field) 187 | } 188 | 189 | return q 190 | } 191 | 192 | //Limit query 193 | func (q *Query) Limit(limit uint16) *Query { 194 | q.limit = limit 195 | return q 196 | } 197 | 198 | //Skip query 199 | func (q *Query) Skip(skip uint16) *Query { 200 | q.skip = skip 201 | return q 202 | } 203 | 204 | //MimeType query 205 | func (q *Query) MimeType(mime string) *Query { 206 | q.mime = mime 207 | return q 208 | } 209 | 210 | //Locale query 211 | func (q *Query) Locale(locale string) *Query { 212 | q.locale = locale 213 | return q 214 | } 215 | 216 | // Values constructs url.Values 217 | func (q *Query) Values() url.Values { 218 | params := url.Values{} 219 | 220 | if q.include != 0 { 221 | if q.include > 10 { 222 | panic("include value should be between 0 and 10") 223 | } 224 | 225 | params.Set("include", strconv.Itoa(int(q.include))) 226 | } 227 | 228 | if q.contentType != "" { 229 | params.Set("content_type", q.contentType) 230 | } 231 | 232 | if len(q.fields) > 0 { 233 | if len(q.fields) > 100 { 234 | panic("You can select up to 100 properties for `select`") 235 | } 236 | 237 | for _, sel := range q.fields { 238 | if len(strings.Split(sel, ".")) > 2 { 239 | panic("you should provide at most 2 depth for `select`") 240 | } 241 | } 242 | 243 | if q.contentType == "" { 244 | panic("you should provide content_type parameter") 245 | } 246 | 247 | params.Set("select", strings.Join(q.fields, ",")) 248 | } 249 | 250 | for k, v := range q.e { 251 | switch reflect.TypeOf(v).Kind() { 252 | case reflect.Int: 253 | { 254 | intV := reflect.ValueOf(v).Interface().(int) 255 | params.Set(k, strconv.Itoa(intV)) 256 | } 257 | case reflect.String: 258 | { 259 | strV := reflect.ValueOf(v).Interface().(string) 260 | params.Set(k, strV) 261 | } 262 | } 263 | } 264 | 265 | for k, v := range q.ne { 266 | switch reflect.TypeOf(v).Kind() { 267 | case reflect.Int: 268 | { 269 | intV := reflect.ValueOf(v).Interface().(int) 270 | params.Set(k+"[ne]", strconv.Itoa(intV)) 271 | } 272 | case reflect.String: 273 | { 274 | strV := reflect.ValueOf(v).Interface().(string) 275 | params.Set(k+"[ne]", strV) 276 | } 277 | } 278 | } 279 | 280 | for k, v := range q.all { 281 | params.Set(k+"[all]", strings.Join(v, ",")) 282 | } 283 | 284 | for k, v := range q.in { 285 | params.Set(k+"[in]", strings.Join(v, ",")) 286 | } 287 | 288 | for k, v := range q.nin { 289 | params.Set(k+"[nin]", strings.Join(v, ",")) 290 | } 291 | 292 | for _, v := range q.exists { 293 | params.Set(v+"[exists]", "true") 294 | } 295 | 296 | for _, v := range q.notExists { 297 | params.Set(v+"[exists]", "false") 298 | } 299 | 300 | for k, v := range q.lt { 301 | switch reflect.TypeOf(v).Kind() { 302 | case reflect.Int: 303 | { 304 | intV := reflect.ValueOf(v).Interface().(int) 305 | params.Set(k+"[lt]", strconv.Itoa(intV)) 306 | } 307 | case reflect.Struct: 308 | { 309 | timeV := reflect.ValueOf(v).Interface().(time.Time) 310 | params.Set(k+"[lt]", timeV.Format("2006-01-02 15:04:05")) 311 | } 312 | } 313 | } 314 | 315 | for k, v := range q.lte { 316 | switch reflect.TypeOf(v).Kind() { 317 | case reflect.Int: 318 | { 319 | intV := reflect.ValueOf(v).Interface().(int) 320 | params.Set(k+"[lte]", strconv.Itoa(intV)) 321 | } 322 | case reflect.Struct: 323 | { 324 | timeV := reflect.ValueOf(v).Interface().(time.Time) 325 | params.Set(k+"[lte]", timeV.Format("2006-01-02 15:04:05")) 326 | } 327 | } 328 | } 329 | 330 | for k, v := range q.gt { 331 | switch reflect.TypeOf(v).Kind() { 332 | case reflect.Int: 333 | { 334 | intV := reflect.ValueOf(v).Interface().(int) 335 | params.Set(k+"[gt]", strconv.Itoa(intV)) 336 | } 337 | case reflect.Struct: 338 | { 339 | timeV := reflect.ValueOf(v).Interface().(time.Time) 340 | params.Set(k+"[gt]", timeV.Format("2006-01-02 15:04:05")) 341 | } 342 | } 343 | } 344 | 345 | for k, v := range q.gte { 346 | switch reflect.TypeOf(v).Kind() { 347 | case reflect.Int: 348 | { 349 | intV := reflect.ValueOf(v).Interface().(int) 350 | params.Set(k+"[gte]", strconv.Itoa(intV)) 351 | } 352 | case reflect.Struct: 353 | { 354 | timeV := reflect.ValueOf(v).Interface().(time.Time) 355 | params.Set(k+"[gte]", timeV.Format("2006-01-02 15:04:05")) 356 | } 357 | } 358 | } 359 | 360 | if q.q != "" { 361 | params.Set("query", q.q) 362 | } 363 | 364 | for k, v := range q.match { 365 | params.Set(k+"[match]", v) 366 | } 367 | 368 | for k, v := range q.near { 369 | params.Set(k+"[near]", v) 370 | } 371 | 372 | for k, v := range q.within { 373 | params.Set(k+"[within]", v) 374 | } 375 | 376 | if len(q.order) > 0 { 377 | // if q.contentType == "" { 378 | // panic("you should provide a content type for order queries") 379 | // } 380 | 381 | params.Set("order", strings.Join(q.order, ",")) 382 | } 383 | 384 | if q.limit != 0 { 385 | if q.limit > 1000 { 386 | panic("limit value should be between 0 and 1000") 387 | } 388 | 389 | params.Set("limit", strconv.Itoa(int(q.limit))) 390 | } 391 | 392 | if q.skip != 0 { 393 | params.Set("skip", strconv.Itoa(int(q.skip))) 394 | } 395 | 396 | if q.mime != "" { 397 | params.Set("mimetype_group", q.mime) 398 | } 399 | 400 | if q.locale != "" { 401 | params.Set("locale", q.locale) 402 | } 403 | 404 | return params 405 | } 406 | 407 | func (q *Query) String() string { 408 | return q.Values().Encode() 409 | } 410 | -------------------------------------------------------------------------------- /query_test.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "net/url" 5 | "strconv" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestQueryInclude(t *testing.T) { 13 | q := NewQuery().Include(5) 14 | expected := url.Values{} 15 | expected.Set("include", "5") 16 | assert.Equal(t, expected.Encode(), q.String()) 17 | 18 | assert.Panics(t, func() { 19 | q := NewQuery().Include(11) 20 | q.String() 21 | }, "out of range `include` should panic") 22 | } 23 | 24 | func TestQueryContentType(t *testing.T) { 25 | q := NewQuery().ContentType("content_type") 26 | expected := url.Values{} 27 | expected.Set("content_type", "content_type") 28 | assert.Equal(t, expected.Encode(), q.String()) 29 | } 30 | 31 | func TestQuerySelect(t *testing.T) { 32 | q := NewQuery(). 33 | ContentType("ct"). 34 | Select([]string{"field1", "field2"}) 35 | 36 | expected := url.Values{} 37 | expected.Set("content_type", "ct") 38 | expected.Set("select", "field1,field2") 39 | assert.Equal(t, expected.Encode(), q.String()) 40 | 41 | assert.Panics(t, func() { 42 | q := NewQuery().Select([]string{"field1", "field2"}) 43 | expected := url.Values{} 44 | expected.Set("select", "field1,field2") 45 | assert.Equal(t, expected.Encode(), q.String()) 46 | }, "select needs content_type") 47 | 48 | assert.Panics(t, func() { 49 | fields := []string{} 50 | for i := 0; i < 110; i++ { 51 | fields = append(fields, "field"+strconv.Itoa(i)) 52 | } 53 | 54 | q := NewQuery().Select(fields) 55 | q.String() 56 | }, "select accepts 100 fields max") 57 | 58 | assert.Panics(t, func() { 59 | q := NewQuery().Select([]string{"field1", "field2.d1", "field3.d2.d3"}) 60 | q.String() 61 | }, "select accepts depths 3 max") 62 | } 63 | 64 | func TestQueryEqual(t *testing.T) { 65 | q := NewQuery().Equal("field1", 10) 66 | expected := url.Values{} 67 | expected.Set("field1", "10") 68 | assert.Equal(t, expected.Encode(), q.String()) 69 | 70 | q = q.Equal("field1", "11") 71 | expected.Set("field1", "11") 72 | assert.Equal(t, expected.Encode(), q.String()) 73 | 74 | q = q.Equal("field1", time.Now()) 75 | expected.Del("field1") 76 | assert.Equal(t, expected.Encode(), q.String()) 77 | } 78 | 79 | func TestQueryNotEqual(t *testing.T) { 80 | q := NewQuery().NotEqual("field1", 10) 81 | expected := url.Values{} 82 | 83 | expected.Set("field1[ne]", "10") 84 | assert.Equal(t, expected.Encode(), q.String()) 85 | 86 | q = q.NotEqual("field1", "11") 87 | expected.Set("field1[ne]", "11") 88 | assert.Equal(t, expected.Encode(), q.String()) 89 | 90 | q = q.NotEqual("field1", time.Now()) 91 | expected.Del("field1[ne]") 92 | assert.Equal(t, expected.Encode(), q.String()) 93 | } 94 | 95 | func TestQueryAll(t *testing.T) { 96 | q := NewQuery().All("field1", []string{"10", "test"}) 97 | expected := url.Values{} 98 | expected.Set("field1[all]", "10,test") 99 | assert.Equal(t, expected.Encode(), q.String()) 100 | } 101 | 102 | func TestQueryIn(t *testing.T) { 103 | q := NewQuery().In("sys.id", []string{"test", "test2"}) 104 | expected := url.Values{} 105 | expected.Set("sys.id[in]", "test,test2") 106 | assert.Equal(t, expected.Encode(), q.String()) 107 | } 108 | 109 | func TestQueryNotIn(t *testing.T) { 110 | q := NewQuery().NotIn("sys.id", []string{"test3"}) 111 | expected := url.Values{} 112 | expected.Set("sys.id[nin]", "test3") 113 | assert.Equal(t, expected.Encode(), q.String()) 114 | } 115 | 116 | func TestQueryExists(t *testing.T) { 117 | q := NewQuery().Exists("sys.id") 118 | expected := url.Values{} 119 | expected.Set("sys.id[exists]", "true") 120 | assert.Equal(t, expected.Encode(), q.String()) 121 | } 122 | 123 | func TestQueryNotExists(t *testing.T) { 124 | q := NewQuery().NotExists("sys.id") 125 | expected := url.Values{} 126 | expected.Set("sys.id[exists]", "false") 127 | assert.Equal(t, expected.Encode(), q.String()) 128 | } 129 | 130 | func TestQueryLessThan(t *testing.T) { 131 | q := NewQuery().LessThan("fields.date", 10) 132 | expected := url.Values{} 133 | expected.Set("fields.date[lt]", "10") 134 | assert.Equal(t, expected.Encode(), q.String()) 135 | 136 | now := time.Now() 137 | q = NewQuery().LessThan("fields.date", now) 138 | expected = url.Values{} 139 | expected.Set("fields.date[lt]", now.Format("2006-01-02 15:04:05")) 140 | assert.Equal(t, expected.Encode(), q.String()) 141 | } 142 | 143 | func TestQueryLessThanOrEqual(t *testing.T) { 144 | q := NewQuery().LessThanOrEqual("fields.date", 10) 145 | expected := url.Values{} 146 | expected.Set("fields.date[lte]", "10") 147 | assert.Equal(t, expected.Encode(), q.String()) 148 | 149 | now := time.Now() 150 | q = NewQuery().LessThanOrEqual("fields.date", now) 151 | expected = url.Values{} 152 | expected.Set("fields.date[lte]", now.Format("2006-01-02 15:04:05")) 153 | assert.Equal(t, expected.Encode(), q.String()) 154 | } 155 | 156 | func TestQueryGreaterThan(t *testing.T) { 157 | q := NewQuery().GreaterThan("fields.date", 10) 158 | expected := url.Values{} 159 | expected.Set("fields.date[gt]", "10") 160 | assert.Equal(t, expected.Encode(), q.String()) 161 | 162 | now := time.Now() 163 | q = NewQuery().GreaterThan("fields.date", now) 164 | expected = url.Values{} 165 | expected.Set("fields.date[gt]", now.Format("2006-01-02 15:04:05")) 166 | assert.Equal(t, expected.Encode(), q.String()) 167 | } 168 | 169 | func TestQueryGreaterThanOrEqual(t *testing.T) { 170 | q := NewQuery().GreaterThanOrEqual("fields.date", 10) 171 | expected := url.Values{} 172 | expected.Set("fields.date[gte]", "10") 173 | assert.Equal(t, expected.Encode(), q.String()) 174 | 175 | now := time.Now() 176 | q = NewQuery().GreaterThanOrEqual("fields.date", now) 177 | expected = url.Values{} 178 | expected.Set("fields.date[gte]", now.Format("2006-01-02 15:04:05")) 179 | assert.Equal(t, expected.Encode(), q.String()) 180 | } 181 | 182 | func TestQueryQuery(t *testing.T) { 183 | q := NewQuery().Query("query_str") 184 | expected := url.Values{} 185 | expected.Set("query", "query_str") 186 | assert.Equal(t, expected.Encode(), q.String()) 187 | } 188 | 189 | func TestQueryMatch(t *testing.T) { 190 | q := NewQuery().Match("field1", "match_query") 191 | expected := url.Values{} 192 | expected.Set("field1[match]", "match_query") 193 | assert.Equal(t, expected.Encode(), q.String()) 194 | } 195 | 196 | func TestQueryNear(t *testing.T) { 197 | q := NewQuery().Near("field1", 38, -120) 198 | expected := url.Values{} 199 | expected.Set("field1[near]", "38,-120") 200 | assert.Equal(t, expected.Encode(), q.String()) 201 | } 202 | 203 | func TestQueryWithin(t *testing.T) { 204 | q := NewQuery().Within("field1", 38, -120, 10, 120) 205 | expected := url.Values{} 206 | expected.Set("field1[within]", "38,-120,10,120") 207 | assert.Equal(t, expected.Encode(), q.String()) 208 | } 209 | 210 | func TestQueryWithinRadius(t *testing.T) { 211 | q := NewQuery().WithinRadius("field1", 38, -120, 22) 212 | expected := url.Values{} 213 | expected.Set("field1[within]", "38,-120,22") 214 | assert.Equal(t, expected.Encode(), q.String()) 215 | } 216 | 217 | func TestQueryOrder(t *testing.T) { 218 | q := NewQuery().ContentType("ct").Order("field1", false) 219 | expected := url.Values{} 220 | expected.Set("content_type", "ct") 221 | expected.Set("order", "field1") 222 | assert.Equal(t, expected.Encode(), q.String()) 223 | 224 | q = NewQuery().ContentType("ct").Order("field1", true) 225 | expected = url.Values{} 226 | expected.Set("content_type", "ct") 227 | expected.Set("order", "-field1") 228 | assert.Equal(t, expected.Encode(), q.String()) 229 | 230 | q = NewQuery(). 231 | ContentType("ct"). 232 | Order("field1", true). 233 | Order("field2", false). 234 | Order("field3", false) 235 | 236 | expected = url.Values{} 237 | expected.Set("content_type", "ct") 238 | expected.Set("order", "-field1,field2,field3") 239 | assert.Equal(t, expected.Encode(), q.String()) 240 | 241 | // assert.Panics(t, func() { 242 | // q := NewQuery().Order("field1", false) 243 | // q.String() 244 | // }, "out of range limit should panic") 245 | } 246 | 247 | func TestQueryLimit(t *testing.T) { 248 | q := NewQuery().Limit(10) 249 | expected := url.Values{} 250 | expected.Set("limit", "10") 251 | assert.Equal(t, expected.Encode(), q.String()) 252 | 253 | assert.Panics(t, func() { 254 | q := NewQuery().Limit(3000) 255 | q.String() 256 | }, "out of range limit should panic") 257 | } 258 | 259 | func TestQuerySkip(t *testing.T) { 260 | q := NewQuery().Skip(10) 261 | expected := url.Values{} 262 | expected.Set("skip", "10") 263 | assert.Equal(t, expected.Encode(), q.String()) 264 | } 265 | 266 | func TestQueryMimeType(t *testing.T) { 267 | q := NewQuery().MimeType("image") 268 | expected := url.Values{} 269 | expected.Set("mimetype_group", "image") 270 | assert.Equal(t, expected.Encode(), q.String()) 271 | } 272 | 273 | func TestQuery(t *testing.T) { 274 | q := NewQuery(). 275 | Equal("cat.name", "catname"). 276 | NotEqual("cat.name", "dogname"). 277 | In("sys.id", []string{"test", "test2"}). 278 | NotIn("sys.id", []string{"test3"}). 279 | LessThan("fields.cat", 4) 280 | 281 | expected := url.Values{} 282 | expected.Set("cat.name", "catname") 283 | expected.Set("cat.name[ne]", "dogname") 284 | expected.Set("sys.id[in]", "test,test2") 285 | expected.Set("sys.id[nin]", "test3") 286 | expected.Set("fields.cat[lt]", "4") 287 | 288 | assert.Equal(t, expected.Encode(), q.String()) 289 | } 290 | -------------------------------------------------------------------------------- /space.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | ) 10 | 11 | // SpacesService model 12 | type SpacesService service 13 | 14 | // Space model 15 | type Space struct { 16 | Sys *Sys `json:"sys,omitempty"` 17 | Name string `json:"name,omitempty"` 18 | DefaultLocale string `json:"defaultLocale,omitempty"` 19 | } 20 | 21 | // MarshalJSON for custom json marshaling 22 | func (space *Space) MarshalJSON() ([]byte, error) { 23 | return json.Marshal(&struct { 24 | Name string `json:"name,omitempty"` 25 | DefaultLocale string `json:"defaultLocale,omitempty"` 26 | }{ 27 | Name: space.Name, 28 | DefaultLocale: space.DefaultLocale, 29 | }) 30 | } 31 | 32 | // GetVersion returns entity version 33 | func (space *Space) GetVersion() int { 34 | version := 1 35 | if space.Sys != nil { 36 | version = space.Sys.Version 37 | } 38 | 39 | return version 40 | } 41 | 42 | // List creates a spaces collection 43 | func (service *SpacesService) List() *Collection { 44 | req, _ := service.c.newRequest("GET", "/spaces", nil, nil) 45 | 46 | col := NewCollection(&CollectionOptions{}) 47 | col.c = service.c 48 | col.req = req 49 | 50 | return col 51 | } 52 | 53 | // Get returns a single space entity 54 | func (service *SpacesService) Get(spaceID string) (*Space, error) { 55 | path := fmt.Sprintf("/spaces/%s", spaceID) 56 | req, err := service.c.newRequest(http.MethodGet, path, nil, nil) 57 | if err != nil { 58 | return &Space{}, err 59 | } 60 | 61 | var space Space 62 | if ok := service.c.do(req, &space); ok != nil { 63 | return &Space{}, ok 64 | } 65 | 66 | return &space, nil 67 | } 68 | 69 | // Upsert updates or creates a new space 70 | func (service *SpacesService) Upsert(space *Space) error { 71 | bytesArray, err := json.Marshal(space) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | var path string 77 | var method string 78 | 79 | if space.Sys != nil && space.Sys.CreatedAt != "" { 80 | path = fmt.Sprintf("/spaces/%s", space.Sys.ID) 81 | method = http.MethodPut 82 | } else { 83 | path = "/spaces" 84 | method = http.MethodPost 85 | } 86 | 87 | req, err := service.c.newRequest(method, path, nil, bytes.NewReader(bytesArray)) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | req.Header.Set("X-Contentful-Version", strconv.Itoa(space.GetVersion())) 93 | 94 | return service.c.do(req, space) 95 | } 96 | 97 | // Delete the given space 98 | func (service *SpacesService) Delete(space *Space) error { 99 | path := fmt.Sprintf("/spaces/%s", space.Sys.ID) 100 | 101 | req, err := service.c.newRequest(http.MethodDelete, path, nil, nil) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | version := strconv.Itoa(space.Sys.Version) 107 | req.Header.Set("X-Contentful-Version", version) 108 | 109 | return service.c.do(req, nil) 110 | } 111 | -------------------------------------------------------------------------------- /space_test.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func ExampleSpacesService_Get() { 15 | cma := NewCMA("cma-token") 16 | 17 | space, err := cma.Spaces.Get("space-id") 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | fmt.Println(space.Name) 23 | } 24 | 25 | func ExampleSpacesService_List() { 26 | cma := NewCMA("cma-token") 27 | collection, err := cma.Spaces.List().Next() 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | spaces := collection.ToSpace() 33 | for _, space := range spaces { 34 | fmt.Println(space.Sys.ID, space.Name) 35 | } 36 | } 37 | 38 | func ExampleSpacesService_Upsert_create() { 39 | cma := NewCMA("cma-token") 40 | 41 | space := &Space{ 42 | Name: "space-name", 43 | DefaultLocale: "en-US", 44 | } 45 | 46 | err := cma.Spaces.Upsert(space) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | } 51 | 52 | func ExampleSpacesService_Upsert_update() { 53 | cma := NewCMA("cma-token") 54 | 55 | space, err := cma.Spaces.Get("space-id") 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | space.Name = "modified" 61 | err = cma.Spaces.Upsert(space) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | } 66 | 67 | func ExampleSpacesService_Delete() { 68 | cma := NewCMA("cma-token") 69 | 70 | space, err := cma.Spaces.Get("space-id") 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | err = cma.Spaces.Delete(space) 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | } 80 | 81 | func ExampleSpacesService_Delete_all() { 82 | cma := NewCMA("cma-token") 83 | 84 | collection, err := cma.Spaces.List().Next() 85 | if err != nil { 86 | log.Fatal(err) 87 | } 88 | 89 | for _, space := range collection.ToSpace() { 90 | err := cma.Spaces.Delete(space) 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | } 95 | } 96 | 97 | func TestSpacesServiceList(t *testing.T) { 98 | var err error 99 | assert := assert.New(t) 100 | 101 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 102 | assert.Equal(r.Method, "GET") 103 | assert.Equal(r.URL.Path, "/spaces") 104 | 105 | checkHeaders(r, assert) 106 | 107 | w.WriteHeader(200) 108 | fmt.Fprintln(w, readTestData("spaces.json")) 109 | }) 110 | 111 | // test server 112 | server := httptest.NewServer(handler) 113 | defer server.Close() 114 | 115 | // cma client 116 | cma = NewCMA(CMAToken) 117 | cma.BaseURL = server.URL 118 | 119 | collection, err := cma.Spaces.List().Next() 120 | assert.Nil(err) 121 | 122 | spaces := collection.ToSpace() 123 | assert.Equal(2, len(spaces)) 124 | assert.Equal("id1", spaces[0].Sys.ID) 125 | assert.Equal("id2", spaces[1].Sys.ID) 126 | } 127 | 128 | func TestSpacesServiceList_Pagination(t *testing.T) { 129 | var err error 130 | assert := assert.New(t) 131 | 132 | requestCount := 1 133 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 134 | assert.Equal(r.Method, "GET") 135 | assert.Equal(r.URL.Path, "/spaces") 136 | checkHeaders(r, assert) 137 | 138 | w.WriteHeader(200) 139 | query := r.URL.Query() 140 | if requestCount == 1 { 141 | assert.Equal(query.Get("order"), "-sys.createdAt") 142 | assert.Equal(query.Get("skip"), "") 143 | fmt.Fprintln(w, readTestData("spaces.json")) 144 | } else { 145 | assert.Equal(query.Get("order"), "-sys.createdAt") 146 | assert.Equal(query.Get("skip"), "100") 147 | fmt.Fprintln(w, readTestData("spaces-page-2.json")) 148 | } 149 | requestCount++ 150 | }) 151 | 152 | // test server 153 | server := httptest.NewServer(handler) 154 | defer server.Close() 155 | 156 | // cma client 157 | cma = NewCMA(CMAToken) 158 | cma.BaseURL = server.URL 159 | 160 | collection, err := cma.Spaces.List().Next() 161 | assert.Nil(err) 162 | 163 | nextPage, err := collection.Next() 164 | assert.Nil(err) 165 | assert.IsType(&Collection{}, nextPage) 166 | } 167 | 168 | func TestSpacesServiceGet(t *testing.T) { 169 | var err error 170 | assert := assert.New(t) 171 | 172 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 173 | assert.Equal(r.Method, "GET") 174 | assert.Equal(r.URL.Path, "/spaces/"+spaceID) 175 | 176 | checkHeaders(r, assert) 177 | 178 | w.WriteHeader(200) 179 | fmt.Fprintln(w, readTestData("space-1.json")) 180 | }) 181 | 182 | // test server 183 | server := httptest.NewServer(handler) 184 | defer server.Close() 185 | 186 | // cma client 187 | cma = NewCMA(CMAToken) 188 | cma.BaseURL = server.URL 189 | 190 | space, err := cma.Spaces.Get(spaceID) 191 | assert.Nil(err) 192 | assert.Equal("id1", space.Sys.ID) 193 | } 194 | 195 | func TestSpaceSaveForCreate(t *testing.T) { 196 | assert := assert.New(t) 197 | 198 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 199 | assert.Equal(r.Method, "POST") 200 | assert.Equal(r.RequestURI, "/spaces") 201 | checkHeaders(r, assert) 202 | 203 | var payload map[string]interface{} 204 | err := json.NewDecoder(r.Body).Decode(&payload) 205 | assert.Nil(err) 206 | assert.Equal("new space", payload["name"]) 207 | assert.Equal("en", payload["defaultLocale"]) 208 | 209 | w.WriteHeader(201) 210 | fmt.Fprintln(w, string(readTestData("spaces-newspace.json"))) 211 | }) 212 | 213 | // test server 214 | server := httptest.NewServer(handler) 215 | defer server.Close() 216 | 217 | // cma client 218 | cma = NewCMA(CMAToken) 219 | cma.BaseURL = server.URL 220 | 221 | space := &Space{ 222 | Name: "new space", 223 | DefaultLocale: "en", 224 | } 225 | 226 | err := cma.Spaces.Upsert(space) 227 | assert.Nil(err) 228 | assert.Equal("newspace", space.Sys.ID) 229 | assert.Equal("new space", space.Name) 230 | assert.Equal("en", space.DefaultLocale) 231 | } 232 | 233 | func TestSpaceSaveForUpdate(t *testing.T) { 234 | var err error 235 | assert := assert.New(t) 236 | 237 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 238 | assert.Equal(r.Method, "PUT") 239 | assert.Equal(r.RequestURI, "/spaces/newspace") 240 | checkHeaders(r, assert) 241 | 242 | var payload map[string]interface{} 243 | err := json.NewDecoder(r.Body).Decode(&payload) 244 | assert.Nil(err) 245 | assert.Equal("changed-space-name", payload["name"]) 246 | assert.Equal("de", payload["defaultLocale"]) 247 | 248 | w.WriteHeader(200) 249 | fmt.Fprintln(w, string(readTestData("spaces-newspace-updated.json"))) 250 | }) 251 | 252 | // test server 253 | server := httptest.NewServer(handler) 254 | defer server.Close() 255 | 256 | // cma client 257 | cma = NewCMA(CMAToken) 258 | cma.BaseURL = server.URL 259 | 260 | space, err := spaceFromTestData("spaces-newspace.json") 261 | assert.Nil(err) 262 | 263 | space.Name = "changed-space-name" 264 | space.DefaultLocale = "de" 265 | 266 | err = cma.Spaces.Upsert(space) 267 | assert.Nil(err) 268 | assert.Equal("changed-space-name", space.Name) 269 | assert.Equal("de", space.DefaultLocale) 270 | assert.Equal(2, space.Sys.Version) 271 | } 272 | 273 | func TestSpaceDelete(t *testing.T) { 274 | var err error 275 | assert := assert.New(t) 276 | 277 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 278 | assert.Equal(r.Method, "DELETE") 279 | assert.Equal(r.RequestURI, "/spaces/"+spaceID) 280 | checkHeaders(r, assert) 281 | 282 | w.WriteHeader(200) 283 | }) 284 | 285 | // test server 286 | server := httptest.NewServer(handler) 287 | defer server.Close() 288 | 289 | // cma client 290 | cma = NewCMA(CMAToken) 291 | cma.BaseURL = server.URL 292 | 293 | space, err := spaceFromTestData("spaces-" + spaceID + ".json") 294 | assert.Nil(err) 295 | 296 | err = cma.Spaces.Delete(space) 297 | assert.Nil(err) 298 | } 299 | -------------------------------------------------------------------------------- /testdata/content_type-updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ct-name-updated", 3 | "description": "ct-description-updated", 4 | "fields": [ 5 | { 6 | "id": "field1", 7 | "name": "field1-name-updated", 8 | "type": "String", 9 | "localized": false, 10 | "required": false, 11 | "disabled": false, 12 | "omitted": false, 13 | "validations": [] 14 | }, 15 | { 16 | "id": "field2", 17 | "name": "field2-name-updated", 18 | "type": "Integer", 19 | "disabled": false, 20 | "localized": false, 21 | "required": false, 22 | "omitted": false, 23 | "validations": [] 24 | }, 25 | { 26 | "id": "field3", 27 | "name": "field3-name", 28 | "type": "Date", 29 | "disabled": false, 30 | "localized": false, 31 | "required": false, 32 | "omitted": false, 33 | "validations": [] 34 | } 35 | ], 36 | "displayField": "field3", 37 | "sys": { 38 | "id": "63Vgs0BFK0USe4i2mQUGK6", 39 | "type": "ContentType", 40 | "version": 2, 41 | "createdAt": "2017-03-20T21:03:59.364Z", 42 | "createdBy": { 43 | "sys": { 44 | "type": "Link", 45 | "linkType": "User", 46 | "id": "7aAReMWo8woRiCCd2wFNwB" 47 | } 48 | }, 49 | "space": { 50 | "sys": { 51 | "type": "Link", 52 | "linkType": "Space", 53 | "id": "q65ipbk62rgw" 54 | } 55 | }, 56 | "updatedAt": "2017-03-20T21:03:59.364Z", 57 | "updatedBy": { 58 | "sys": { 59 | "type": "Link", 60 | "linkType": "User", 61 | "id": "7aAReMWo8woRiCCd2wFNwB" 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /testdata/content_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ct-name", 3 | "description": "ct-description", 4 | "fields": [ 5 | { 6 | "id": "field1", 7 | "name": "field1-name", 8 | "type": "Symbol", 9 | "localized": false, 10 | "required": true, 11 | "disabled": false, 12 | "omitted": false, 13 | "validations": [] 14 | }, 15 | { 16 | "id": "field2", 17 | "name": "field2-name", 18 | "type": "Symbol", 19 | "disabled": true, 20 | "localized": false, 21 | "required": false, 22 | "omitted": false, 23 | "validations": [] 24 | } 25 | ], 26 | "displayField": "field1", 27 | "sys": { 28 | "id": "63Vgs0BFK0USe4i2mQUGK6", 29 | "type": "ContentType", 30 | "version": 1, 31 | "createdAt": "2017-03-20T21:03:59.364Z", 32 | "createdBy": { 33 | "sys": { 34 | "type": "Link", 35 | "linkType": "User", 36 | "id": "7aAReMWo8woRiCCd2wFNwB" 37 | } 38 | }, 39 | "space": { 40 | "sys": { 41 | "type": "Link", 42 | "linkType": "Space", 43 | "id": "q65ipbk62rgw" 44 | } 45 | }, 46 | "updatedAt": "2017-03-20T21:03:59.364Z", 47 | "updatedBy": { 48 | "sys": { 49 | "type": "Link", 50 | "linkType": "User", 51 | "id": "7aAReMWo8woRiCCd2wFNwB" 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /testdata/content_type_with_validations.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "validations-test", 3 | "fields": [ 4 | { 5 | "name": "text-short", 6 | "id": "HbvLK9kzF91K9byY", 7 | "apiName": "textShort", 8 | "type": "Symbol", 9 | "required": true, 10 | "validations": [ 11 | { 12 | "unique": true 13 | }, 14 | { 15 | "size": { 16 | "min": 10, 17 | "max": 20 18 | }, 19 | "message": "text-short range error message" 20 | }, 21 | { 22 | "regexp": { 23 | "pattern": "^\\w[\\w.-]*@([\\w-]+\\.)+[\\w-]+$", 24 | "flags": "g" 25 | }, 26 | "message": "text-short regex error message" 27 | }, 28 | { 29 | "in": [ 30 | "test", 31 | "3", 32 | "5" 33 | ], 34 | "message": "text-short predefined values error message" 35 | } 36 | ], 37 | "localized": false, 38 | "disabled": false, 39 | "omitted": false 40 | }, 41 | { 42 | "name": "text-long", 43 | "id": "y5NKeXJImJidOLOF", 44 | "apiName": "textLong", 45 | "type": "Text", 46 | "required": true, 47 | "validations": [ 48 | { 49 | "size": { 50 | "min": 5, 51 | "max": 19 52 | }, 53 | "message": "text-long range error message" 54 | }, 55 | { 56 | "regexp": { 57 | "pattern": "^(0?[1-9]|[12][0-9]|3[01])[- \\/.](0?[1-9]|1[012])[- \\/.](19|20)?\\d\\d$", 58 | "flags": "g" 59 | }, 60 | "message": "text-long regex error message" 61 | }, 62 | { 63 | "in": [ 64 | "val" 65 | ], 66 | "message": "text-long predefined values custom error message" 67 | } 68 | ] 69 | }, 70 | { 71 | "name": "number-integer", 72 | "id": "fYw4yVZHkAIYe4JB", 73 | "apiName": "numberInteger", 74 | "type": "Integer", 75 | "required": true, 76 | "validations": [ 77 | { 78 | "unique": true 79 | }, 80 | { 81 | "range": { 82 | "min": 10, 83 | "max": 20 84 | }, 85 | "message": "number-integer range error message" 86 | }, 87 | { 88 | "in": [ 89 | 12, 90 | 14 91 | ], 92 | "message": "number-integer predefined values error values" 93 | } 94 | ] 95 | }, 96 | { 97 | "name": "number-decimal", 98 | "id": "e7xPyLeknXzWHCAJ", 99 | "apiName": "numberDecimal", 100 | "type": "Number", 101 | "required": true, 102 | "validations": [ 103 | { 104 | "unique": true 105 | }, 106 | { 107 | "range": { 108 | "min": 5 109 | }, 110 | "message": "number-decimal range error message" 111 | }, 112 | { 113 | "in": [ 114 | 6, 115 | 19 116 | ], 117 | "message": "number-decimal predefined values error message" 118 | } 119 | ] 120 | }, 121 | { 122 | "name": "date", 123 | "id": "FHjBxcapHJXJdlkE", 124 | "apiName": "date", 125 | "type": "Date", 126 | "required": true, 127 | "validations": [ 128 | { 129 | "dateRange": { 130 | "after": null, 131 | "before": null, 132 | "min": "2017-03-15T10:00:00", 133 | "max": "2017-03-31T11:00:00" 134 | }, 135 | "message": "date error message" 136 | } 137 | ] 138 | }, 139 | { 140 | "name": "location", 141 | "id": "oX4Cv7v26o64YNXX", 142 | "apiName": "location", 143 | "type": "Location", 144 | "required": true, 145 | "validations": [] 146 | }, 147 | { 148 | "name": "media-onefile", 149 | "id": "yW00FKvHHYGqNIgW", 150 | "apiName": "mediaOnefile", 151 | "type": "Link", 152 | "linkType": "Asset", 153 | "required": true, 154 | "validations": [ 155 | { 156 | "linkMimetypeGroup": [ 157 | "attachment", 158 | "plaintext", 159 | "image", 160 | "audio", 161 | "video", 162 | "richtext", 163 | "presentation", 164 | "spreadsheet", 165 | "pdfdocument", 166 | "archive", 167 | "code", 168 | "markup" 169 | ], 170 | "message": "media-onefile mime type error message" 171 | }, 172 | { 173 | "assetImageDimensions": { 174 | "width": { 175 | "min": 100, 176 | "max": 200 177 | }, 178 | "height": { 179 | "min": 100, 180 | "max": 200 181 | } 182 | }, 183 | "message": "media-onefile dimension error message" 184 | }, 185 | { 186 | "assetFileSize": { 187 | "min": 10485760, 188 | "max": 30408704 189 | }, 190 | "message": "media-onefile file size error message" 191 | } 192 | ] 193 | }, 194 | { 195 | "name": "media-manyfiles", 196 | "id": "Aomif1Mekh2BN2vo", 197 | "apiName": "mediaManyfiles", 198 | "type": "Array", 199 | "items": { 200 | "type": "Link", 201 | "linkType": "Asset", 202 | "validations": [ 203 | { 204 | "linkMimetypeGroup": [ 205 | "attachment", 206 | "plaintext", 207 | "image", 208 | "audio", 209 | "video", 210 | "richtext", 211 | "spreadsheet", 212 | "pdfdocument", 213 | "archive", 214 | "code", 215 | "markup" 216 | ], 217 | "message": "media-manyfiles error message" 218 | }, 219 | { 220 | "assetImageDimensions": { 221 | "width": { 222 | "min": 200, 223 | "max": 200 224 | }, 225 | "height": { 226 | "min": 300, 227 | "max": 400 228 | } 229 | }, 230 | "message": "media-manufiles dimensin error message" 231 | }, 232 | { 233 | "assetFileSize": { 234 | "min": 20480, 235 | "max": 40960 236 | }, 237 | "message": "media-manyfiles file size error message" 238 | } 239 | ] 240 | }, 241 | "required": true, 242 | "validations": [ 243 | { 244 | "size": { 245 | "min": 2, 246 | "max": 4 247 | }, 248 | "message": "media-manyfiles accept only error message" 249 | } 250 | ] 251 | }, 252 | { 253 | "name": "bool", 254 | "id": "kNXc1q71qKHyaqWi", 255 | "apiName": "bool", 256 | "type": "Boolean", 257 | "required": true, 258 | "validations": [] 259 | }, 260 | { 261 | "name": "json", 262 | "id": "gAh5d0vzBG5XNYQL", 263 | "apiName": "json", 264 | "type": "Object", 265 | "required": true, 266 | "validations": [ 267 | { 268 | "size": { 269 | "min": 30, 270 | "max": 40 271 | }, 272 | "message": "json number of prop error message" 273 | } 274 | ] 275 | }, 276 | { 277 | "name": "ref-oneref", 278 | "id": "LGcZi0zOZ5oVbZG1", 279 | "apiName": "refOneref", 280 | "type": "Link", 281 | "linkType": "Entry", 282 | "required": true, 283 | "validations": [ 284 | { 285 | "linkContentType": [ 286 | "sFzTZbSuM8coEwygeUYes", 287 | "6XwpTaSiiI2Ak2Ww0oi6qa", 288 | "2PqfXUJwE8qSYKuM0U6w8M" 289 | ], 290 | "message": "ref-oneref accept types error message" 291 | } 292 | ] 293 | }, 294 | { 295 | "name": "ref-manyRefs", 296 | "id": "MB0nzivjE8vyIgCi", 297 | "apiName": "refManyRefs", 298 | "type": "Array", 299 | "items": { 300 | "type": "Link", 301 | "linkType": "Entry", 302 | "validations": [ 303 | { 304 | "linkContentType": [ 305 | "sFzTZbSuM8coEwygeUYes", 306 | "6XwpTaSiiI2Ak2Ww0oi6qa", 307 | "2PqfXUJwE8qSYKuM0U6w8M" 308 | ], 309 | "message": "ref-manyRefs accept type error message" 310 | } 311 | ] 312 | }, 313 | "required": true, 314 | "validations": [ 315 | { 316 | "size": { 317 | "min": 2, 318 | "max": 3 319 | }, 320 | "message": "ref-manyRefs number of entries error message" 321 | } 322 | ] 323 | } 324 | ], 325 | "displayField": "HbvLK9kzF91K9byY", 326 | "description": "", 327 | "sys": { 328 | "id": "validationsTest", 329 | "type": "ContentType", 330 | "createdAt": "2017-03-28T09:37:59.869Z", 331 | "createdBy": { 332 | "sys": { 333 | "type": "Link", 334 | "linkType": "User", 335 | "id": "7aAReMWo8woRiCCd2wFNwB" 336 | } 337 | }, 338 | "space": { 339 | "sys": { 340 | "type": "Link", 341 | "linkType": "Space", 342 | "id": "d3e3f3blz3ih" 343 | } 344 | }, 345 | "firstPublishedAt": "2017-03-28T09:38:00.250Z", 346 | "publishedCounter": 4, 347 | "publishedAt": "2017-03-28T09:54:16.774Z", 348 | "publishedBy": { 349 | "sys": { 350 | "type": "Link", 351 | "linkType": "User", 352 | "id": "7aAReMWo8woRiCCd2wFNwB" 353 | } 354 | }, 355 | "publishedVersion": 7, 356 | "version": 9, 357 | "updatedAt": "2017-03-28T10:47:55.699Z", 358 | "updatedBy": { 359 | "sys": { 360 | "type": "Link", 361 | "linkType": "User", 362 | "id": "7aAReMWo8woRiCCd2wFNwB" 363 | } 364 | } 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /testdata/content_types.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "total": 4, 6 | "skip": 0, 7 | "limit": 100, 8 | "items": [ 9 | { 10 | "sys": { 11 | "space": { 12 | "sys": { 13 | "type": "Link", 14 | "linkType": "Space", 15 | "id": "cfexampleapi" 16 | } 17 | }, 18 | "id": "1t9IbcfdCk6m04uISSsaIK", 19 | "type": "ContentType", 20 | "createdAt": "2014-02-21T13:42:33.009Z", 21 | "updatedAt": "2014-02-21T13:42:33.009Z", 22 | "revision": 1 23 | }, 24 | "displayField": "name", 25 | "name": "City", 26 | "description": null, 27 | "fields": [ 28 | { 29 | "id": "name", 30 | "name": "Name", 31 | "type": "Text", 32 | "localized": false, 33 | "required": true, 34 | "disabled": false, 35 | "omitted": false 36 | }, 37 | { 38 | "id": "center", 39 | "name": "Center", 40 | "type": "Location", 41 | "localized": false, 42 | "required": true, 43 | "disabled": false, 44 | "omitted": false 45 | } 46 | ] 47 | }, 48 | { 49 | "sys": { 50 | "space": { 51 | "sys": { 52 | "type": "Link", 53 | "linkType": "Space", 54 | "id": "cfexampleapi" 55 | } 56 | }, 57 | "id": "human", 58 | "type": "ContentType", 59 | "createdAt": "2013-06-27T22:46:14.133Z", 60 | "updatedAt": "2013-09-02T15:10:26.818Z", 61 | "revision": 3 62 | }, 63 | "displayField": "name", 64 | "name": "Human", 65 | "description": null, 66 | "fields": [ 67 | { 68 | "id": "name", 69 | "name": "Name", 70 | "type": "Text", 71 | "localized": false, 72 | "required": true, 73 | "disabled": false, 74 | "omitted": false 75 | }, 76 | { 77 | "id": "description", 78 | "name": "Description", 79 | "type": "Text", 80 | "localized": false, 81 | "required": false, 82 | "disabled": false, 83 | "omitted": false 84 | }, 85 | { 86 | "id": "likes", 87 | "name": "Likes", 88 | "type": "Array", 89 | "localized": false, 90 | "required": false, 91 | "disabled": false, 92 | "omitted": false, 93 | "items": { 94 | "type": "Symbol", 95 | "validations": [] 96 | } 97 | }, 98 | { 99 | "id": "image", 100 | "name": "Image", 101 | "type": "Array", 102 | "localized": false, 103 | "required": false, 104 | "disabled": true, 105 | "omitted": false, 106 | "items": { 107 | "type": "Link", 108 | "validations": [], 109 | "linkType": "Asset" 110 | } 111 | } 112 | ] 113 | }, 114 | { 115 | "sys": { 116 | "space": { 117 | "sys": { 118 | "type": "Link", 119 | "linkType": "Space", 120 | "id": "cfexampleapi" 121 | } 122 | }, 123 | "id": "dog", 124 | "type": "ContentType", 125 | "createdAt": "2013-06-27T22:46:13.498Z", 126 | "updatedAt": "2013-09-02T14:32:11.837Z", 127 | "revision": 2 128 | }, 129 | "displayField": "name", 130 | "name": "Dog", 131 | "description": "Bark!", 132 | "fields": [ 133 | { 134 | "id": "name", 135 | "name": "Name", 136 | "type": "Text", 137 | "localized": false, 138 | "required": true, 139 | "disabled": false, 140 | "omitted": false 141 | }, 142 | { 143 | "id": "description", 144 | "name": "Description", 145 | "type": "Text", 146 | "localized": false, 147 | "required": false, 148 | "disabled": false, 149 | "omitted": false 150 | }, 151 | { 152 | "id": "image", 153 | "name": "Image", 154 | "type": "Link", 155 | "localized": false, 156 | "required": false, 157 | "disabled": false, 158 | "omitted": false, 159 | "linkType": "Asset" 160 | } 161 | ] 162 | }, 163 | { 164 | "sys": { 165 | "space": { 166 | "sys": { 167 | "type": "Link", 168 | "linkType": "Space", 169 | "id": "cfexampleapi" 170 | } 171 | }, 172 | "id": "cat", 173 | "type": "ContentType", 174 | "createdAt": "2013-06-27T22:46:12.852Z", 175 | "updatedAt": "2016-11-21T15:01:43.860Z", 176 | "revision": 6 177 | }, 178 | "displayField": "name", 179 | "name": "Cat", 180 | "description": "Meow.", 181 | "fields": [ 182 | { 183 | "id": "name", 184 | "name": "Name", 185 | "type": "Text", 186 | "localized": true, 187 | "required": true, 188 | "disabled": false, 189 | "omitted": false 190 | }, 191 | { 192 | "id": "likes", 193 | "name": "Likes", 194 | "type": "Array", 195 | "localized": false, 196 | "required": false, 197 | "disabled": false, 198 | "omitted": false, 199 | "items": { 200 | "type": "Symbol", 201 | "validations": [] 202 | } 203 | }, 204 | { 205 | "id": "color", 206 | "name": "Color", 207 | "type": "Symbol", 208 | "localized": false, 209 | "required": false, 210 | "disabled": false, 211 | "omitted": false 212 | }, 213 | { 214 | "id": "bestFriend", 215 | "name": "Best Friend", 216 | "type": "Link", 217 | "localized": false, 218 | "required": false, 219 | "disabled": false, 220 | "omitted": false, 221 | "linkType": "Entry" 222 | }, 223 | { 224 | "id": "birthday", 225 | "name": "Birthday", 226 | "type": "Date", 227 | "localized": false, 228 | "required": false, 229 | "disabled": false, 230 | "omitted": false 231 | }, 232 | { 233 | "id": "lifes", 234 | "name": "Lifes left", 235 | "type": "Integer", 236 | "localized": false, 237 | "required": false, 238 | "disabled": true, 239 | "omitted": false 240 | }, 241 | { 242 | "id": "lives", 243 | "name": "Lives left", 244 | "type": "Integer", 245 | "localized": false, 246 | "required": false, 247 | "disabled": false, 248 | "omitted": false 249 | }, 250 | { 251 | "id": "image", 252 | "name": "Image", 253 | "type": "Link", 254 | "localized": false, 255 | "required": false, 256 | "disabled": false, 257 | "omitted": false, 258 | "linkType": "Asset" 259 | } 260 | ] 261 | } 262 | ] 263 | } 264 | -------------------------------------------------------------------------------- /testdata/entry_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "space": { 4 | "sys": { 5 | "type": "Link", 6 | "linkType": "Space", 7 | "id": "cfexampleapi" 8 | } 9 | }, 10 | "id": "foocat", 11 | "type": "Entry", 12 | "createdAt": "2013-06-27T22:46:19.513Z", 13 | "updatedAt": "2013-09-04T09:19:39.027Z", 14 | "revision": 5, 15 | "contentType": { 16 | "sys": { 17 | "type": "Link", 18 | "linkType": "ContentType", 19 | "id": "cat" 20 | } 21 | } 22 | }, 23 | "fields": { 24 | "name": { 25 | "en-US": "Nyan Cat", 26 | "tlh": "Nyan vIghro'" 27 | }, 28 | "likes": { 29 | "en-US": [ 30 | "rainbows", 31 | "fish" 32 | ] 33 | }, 34 | "color": { 35 | "en-US": "rainbow" 36 | }, 37 | "bestFriend": { 38 | "en-US": { 39 | "sys": { 40 | "type": "Link", 41 | "linkType": "Entry", 42 | "id": "happycat" 43 | } 44 | } 45 | }, 46 | "birthday": { 47 | "en-US": "2011-04-04T22:00:00+00:00" 48 | }, 49 | "lives": { 50 | "en-US": 1337 51 | }, 52 | "image": { 53 | "en-US": { 54 | "sys": { 55 | "type": "Link", 56 | "linkType": "Asset", 57 | "id": "nyancat" 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /testdata/error-forbidden.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestId": "b2c65431d067ad4f58ea290720e9b43d", 3 | "message": "You are not authorized to do this action or exceeded your plan limits.", 4 | "sys": { 5 | "type": "Error", 6 | "id": "Forbidden" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /testdata/error-notfound.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestId": "request-id", 3 | "message": "The resource could not be found.", 4 | "sys": { 5 | "type": "Error", 6 | "id": "NotFound" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /testdata/error-ratelimit.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestId":"request-id", 3 | "message":"You are creating too many Spaces.", 4 | "sys":{ 5 | "type":"Error", 6 | "id":"RateLimitExceeded" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /testdata/locale_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "U.S. English", 3 | "internal_code": "en-US", 4 | "code": "en-US", 5 | "fallbackCode": null, 6 | "default": true, 7 | "contentManagementApi": true, 8 | "contentDeliveryApi": true, 9 | "optional": false, 10 | "sys": { 11 | "type": "Locale", 12 | "id": "4aGeQYgByqQFJtToAOh2JJ", 13 | "version": 0, 14 | "space": { 15 | "sys": { 16 | "type": "Link", 17 | "linkType": "Space", 18 | "id": "222ru4k10hm8" 19 | } 20 | }, 21 | "createdBy": { 22 | "sys": { 23 | "type": "Link", 24 | "linkType": "User", 25 | "id": "50ZPikUCLbyl9V6zQZNWSL" 26 | } 27 | }, 28 | "createdAt": "2016-12-21T12:25:51Z", 29 | "updatedBy": { 30 | "sys": { 31 | "type": "Link", 32 | "linkType": "User", 33 | "id": "50ZPikUCLbyl9V6zQZNWSL" 34 | } 35 | }, 36 | "updatedAt": "2016-12-21T12:25:51Z" 37 | } 38 | } -------------------------------------------------------------------------------- /testdata/locales.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "total": 0, 6 | "skip": 0, 7 | "limit": 100, 8 | "items": [ 9 | { 10 | "name": "English (United States)", 11 | "code": "en-US", 12 | "fallbackCode": "en-US", 13 | "contentDeliveryApi": true, 14 | "contentManagementApi": true, 15 | "default": false, 16 | "optional": false, 17 | "sys": { 18 | "type": "Locale", 19 | "id": "34N35DoyUQAtaKwWTgZs34", 20 | "version": 0, 21 | "space": { 22 | "sys": { 23 | "type": "Link", 24 | "linkType": "Space", 25 | "id": "yadj1kx9rmg0" 26 | } 27 | }, 28 | "createdAt": "2015-05-18T11:29:46.809Z", 29 | "createdBy": { 30 | "sys": { 31 | "type": "Link", 32 | "linkType": "User", 33 | "id": "4FLrUHftHW3v2BLi9fzfjU" 34 | } 35 | }, 36 | "updatedAt": "2015-05-18T11:29:46.809Z", 37 | "updatedBy": { 38 | "sys": { 39 | "type": "Link", 40 | "linkType": "User", 41 | "id": "4FLrUHftHW3v2BLi9fzfjU" 42 | } 43 | } 44 | } 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /testdata/space-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Space", 4 | "id": "id1", 5 | "createdAt": "somedate", 6 | "version": 3 7 | }, 8 | "name": "Contentful Example API", 9 | "locales": [ 10 | { 11 | "code": "en-US", 12 | "default": true, 13 | "name": "English", 14 | "fallbackCode": null 15 | }, 16 | { 17 | "code": "tlh", 18 | "default": false, 19 | "name": "Klingon", 20 | "fallbackCode": "en-US" 21 | }, 22 | { 23 | "code": "bc-BC", 24 | "default": false, 25 | "name": "Baconglish", 26 | "fallbackCode": "tlh" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /testdata/spaces-id1-assets-1x0xpXu4pSGS4OukSyWGUK.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "space": { 4 | "sys": { 5 | "type": "Link", 6 | "linkType": "Space", 7 | "id": "cfexampleapi" 8 | } 9 | }, 10 | "id": "1x0xpXu4pSGS4OukSyWGUK", 11 | "type": "Asset", 12 | "createdAt": "2013-11-06T09:45:10.000Z", 13 | "updatedAt": "2013-12-18T13:27:14.917Z", 14 | "revision": 6, 15 | "locale": "en-US" 16 | }, 17 | "fields": { 18 | "title": "Doge", 19 | "description": "nice picture", 20 | "file": { 21 | "url": "//images.contentful.com/cfexampleapi/1x0xpXu4pSGS4OukSyWGUK/cc1239c6385428ef26f4180190532818/doge.jpg", 22 | "details": { 23 | "size": 522943, 24 | "image": { 25 | "width": 5800, 26 | "height": 4350 27 | } 28 | }, 29 | "fileName": "doge.jpg", 30 | "contentType": "image/jpeg" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /testdata/spaces-id1-assets-3HNzx9gvJScKku4UmcekYw.json: -------------------------------------------------------------------------------- 1 | { 2 | "fields": { 3 | "file": { 4 | "en-US": { 5 | "fileName": "d3b8dad44e5066cfb805e2357469ee64.png", 6 | "contentType": "image/png", 7 | "details": { 8 | "image": { 9 | "width": 206, 10 | "height": 79 11 | }, 12 | "size": 6198 13 | }, 14 | "url": "//images.flinkly.com/222ru4k10hm8/3HNzx9gvJScKku4UmcekYw/997663077456077dde5b5be9bd3c1386/d3b8dad44e5066cfb805e2357469ee64.png" 15 | }, 16 | "de": { 17 | "fileName": "d3b8dad44e5066cfb805e2357469ee64.png", 18 | "contentType": "image/png", 19 | "details": { 20 | "image": { 21 | "width": 206, 22 | "height": 79 23 | }, 24 | "size": 6198 25 | }, 26 | "url": "//images.flinkly.com/222ru4k10hm8/3HNzx9gvJScKku4UmcekYw/997663077456077dde5b5be9bd3c1386/d3b8dad44e5066cfb805e2357469ee64.png-de" 27 | } 28 | }, 29 | "title": { 30 | "en-US": "hehehe", 31 | "de": "hehehe-de" 32 | }, 33 | "description": { 34 | "en-US": "asdfasf", 35 | "de": "asdfasf-de" 36 | } 37 | }, 38 | "sys": { 39 | "id": "3HNzx9gvJScKku4UmcekYw", 40 | "type": "Asset", 41 | "createdAt": "2017-03-13T08:57:35.075Z", 42 | "createdBy": { 43 | "sys": { 44 | "type": "Link", 45 | "linkType": "User", 46 | "id": "50ZPikUCLbyl9V6zQZNWSL" 47 | } 48 | }, 49 | "space": { 50 | "sys": { 51 | "type": "Link", 52 | "linkType": "Space", 53 | "id": "222ru4k10hm8" 54 | } 55 | }, 56 | "firstPublishedAt": "2017-03-13T08:58:05.490Z", 57 | "publishedCounter": 1, 58 | "publishedAt": "2017-03-13T08:58:05.490Z", 59 | "publishedBy": { 60 | "sys": { 61 | "type": "Link", 62 | "linkType": "User", 63 | "id": "50ZPikUCLbyl9V6zQZNWSL" 64 | } 65 | }, 66 | "publishedVersion": 8, 67 | "version": 9, 68 | "updatedAt": "2017-03-13T08:58:05.535Z", 69 | "updatedBy": { 70 | "sys": { 71 | "type": "Link", 72 | "linkType": "User", 73 | "id": "50ZPikUCLbyl9V6zQZNWSL" 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /testdata/spaces-id1-assets-happycat.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "space": { 4 | "sys": { 5 | "type": "Link", 6 | "linkType": "Space", 7 | "id": "cfexampleapi" 8 | } 9 | }, 10 | "id": "happycat", 11 | "type": "Asset", 12 | "createdAt": "2013-09-02T14:56:34.267Z", 13 | "updatedAt": "2013-09-02T15:11:24.361Z", 14 | "revision": 2, 15 | "locale": "en-US" 16 | }, 17 | "fields": { 18 | "title": "Happy Cat", 19 | "file": { 20 | "url": "//images.contentful.com/cfexampleapi/3MZPnjZTIskAIIkuuosCss/382a48dfa2cb16c47aa2c72f7b23bf09/happycatw.jpg", 21 | "details": { 22 | "size": 59939, 23 | "image": { 24 | "width": 273, 25 | "height": 397 26 | } 27 | }, 28 | "fileName": "happycatw.jpg", 29 | "contentType": "image/jpeg" 30 | } 31 | } 32 | }, 33 | -------------------------------------------------------------------------------- /testdata/spaces-id1-assets-nyancat.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "space": { 4 | "sys": { 5 | "type": "Link", 6 | "linkType": "Space", 7 | "id": "cfexampleapi" 8 | } 9 | }, 10 | "id": "nyancat", 11 | "type": "Asset", 12 | "createdAt": "2013-09-02T14:56:34.240Z", 13 | "updatedAt": "2013-09-02T14:56:34.240Z", 14 | "revision": 1, 15 | "locale": "en-US" 16 | }, 17 | "fields": { 18 | "title": "Nyan Cat", 19 | "file": { 20 | "url": "//images.contentful.com/cfexampleapi/4gp6taAwW4CmSgumq2ekUm/9da0cd1936871b8d72343e895a00d611/Nyan_cat_250px_frame.png", 21 | "details": { 22 | "size": 12273, 23 | "image": { 24 | "width": 250, 25 | "height": 250 26 | } 27 | }, 28 | "fileName": "Nyan_cat_250px_frame.png", 29 | "contentType": "image/png" 30 | } 31 | } 32 | }, 33 | -------------------------------------------------------------------------------- /testdata/spaces-id1-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "total": 5, 6 | "skip": 0, 7 | "limit": 100, 8 | "items": [ 9 | { 10 | "sys": { 11 | "space": { 12 | "sys": { 13 | "type": "Link", 14 | "linkType": "Space", 15 | "id": "cfexampleapi" 16 | } 17 | }, 18 | "id": "1x0xpXu4pSGS4OukSyWGUK", 19 | "type": "Asset", 20 | "createdAt": "2013-11-06T09:45:10.000Z", 21 | "updatedAt": "2013-12-18T13:27:14.917Z", 22 | "revision": 6, 23 | "locale": "en-US" 24 | }, 25 | "fields": { 26 | "title": "Doge", 27 | "description": "nice picture", 28 | "file": { 29 | "url": "//images.contentful.com/cfexampleapi/1x0xpXu4pSGS4OukSyWGUK/cc1239c6385428ef26f4180190532818/doge.jpg", 30 | "details": { 31 | "size": 522943, 32 | "image": { 33 | "width": 5800, 34 | "height": 4350 35 | } 36 | }, 37 | "fileName": "doge.jpg", 38 | "contentType": "image/jpeg" 39 | } 40 | } 41 | }, 42 | { 43 | "fields": { 44 | "file": { 45 | "en-US": { 46 | "fileName": "d3b8dad44e5066cfb805e2357469ee64.png", 47 | "contentType": "image/png", 48 | "details": { 49 | "image": { 50 | "width": 206, 51 | "height": 79 52 | }, 53 | "size": 6198 54 | }, 55 | "url": "//images.flinkly.com/222ru4k10hm8/3HNzx9gvJScKku4UmcekYw/997663077456077dde5b5be9bd3c1386/d3b8dad44e5066cfb805e2357469ee64.png" 56 | }, 57 | "de": { 58 | "fileName": "d3b8dad44e5066cfb805e2357469ee64.png", 59 | "contentType": "image/png", 60 | "details": { 61 | "image": { 62 | "width": 206, 63 | "height": 79 64 | }, 65 | "size": 6198 66 | }, 67 | "url": "//images.flinkly.com/222ru4k10hm8/3HNzx9gvJScKku4UmcekYw/997663077456077dde5b5be9bd3c1386/d3b8dad44e5066cfb805e2357469ee64.png-de" 68 | } 69 | }, 70 | "title": { 71 | "en-US": "hehehe", 72 | "de": "hehehe-de" 73 | }, 74 | "description": { 75 | "en-US": "asdfasf", 76 | "de": "asdfasf-de" 77 | } 78 | }, 79 | "sys": { 80 | "id": "3HNzx9gvJScKku4UmcekYw", 81 | "type": "Asset", 82 | "createdAt": "2017-03-13T08:57:35.075Z", 83 | "createdBy": { 84 | "sys": { 85 | "type": "Link", 86 | "linkType": "User", 87 | "id": "50ZPikUCLbyl9V6zQZNWSL" 88 | } 89 | }, 90 | "space": { 91 | "sys": { 92 | "type": "Link", 93 | "linkType": "Space", 94 | "id": "222ru4k10hm8" 95 | } 96 | }, 97 | "firstPublishedAt": "2017-03-13T08:58:05.490Z", 98 | "publishedCounter": 1, 99 | "publishedAt": "2017-03-13T08:58:05.490Z", 100 | "publishedBy": { 101 | "sys": { 102 | "type": "Link", 103 | "linkType": "User", 104 | "id": "50ZPikUCLbyl9V6zQZNWSL" 105 | } 106 | }, 107 | "publishedVersion": 8, 108 | "version": 9, 109 | "updatedAt": "2017-03-13T08:58:05.535Z", 110 | "updatedBy": { 111 | "sys": { 112 | "type": "Link", 113 | "linkType": "User", 114 | "id": "50ZPikUCLbyl9V6zQZNWSL" 115 | } 116 | } 117 | } 118 | }, 119 | { 120 | "sys": { 121 | "space": { 122 | "sys": { 123 | "type": "Link", 124 | "linkType": "Space", 125 | "id": "cfexampleapi" 126 | } 127 | }, 128 | "id": "happycat", 129 | "type": "Asset", 130 | "createdAt": "2013-09-02T14:56:34.267Z", 131 | "updatedAt": "2013-09-02T15:11:24.361Z", 132 | "revision": 2, 133 | "locale": "en-US" 134 | }, 135 | "fields": { 136 | "title": "Happy Cat", 137 | "file": { 138 | "url": "//images.contentful.com/cfexampleapi/3MZPnjZTIskAIIkuuosCss/382a48dfa2cb16c47aa2c72f7b23bf09/happycatw.jpg", 139 | "details": { 140 | "size": 59939, 141 | "image": { 142 | "width": 273, 143 | "height": 397 144 | } 145 | }, 146 | "fileName": "happycatw.jpg", 147 | "contentType": "image/jpeg" 148 | } 149 | } 150 | }, 151 | { 152 | "sys": { 153 | "space": { 154 | "sys": { 155 | "type": "Link", 156 | "linkType": "Space", 157 | "id": "cfexampleapi" 158 | } 159 | }, 160 | "id": "nyancat", 161 | "type": "Asset", 162 | "createdAt": "2013-09-02T14:56:34.240Z", 163 | "updatedAt": "2013-09-02T14:56:34.240Z", 164 | "revision": 1, 165 | "locale": "en-US" 166 | }, 167 | "fields": { 168 | "title": "Nyan Cat", 169 | "file": { 170 | "url": "//images.contentful.com/cfexampleapi/4gp6taAwW4CmSgumq2ekUm/9da0cd1936871b8d72343e895a00d611/Nyan_cat_250px_frame.png", 171 | "details": { 172 | "size": 12273, 173 | "image": { 174 | "width": 250, 175 | "height": 250 176 | } 177 | }, 178 | "fileName": "Nyan_cat_250px_frame.png", 179 | "contentType": "image/png" 180 | } 181 | } 182 | }, 183 | { 184 | "sys": { 185 | "space": { 186 | "sys": { 187 | "type": "Link", 188 | "linkType": "Space", 189 | "id": "cfexampleapi" 190 | } 191 | }, 192 | "id": "jake", 193 | "type": "Asset", 194 | "createdAt": "2013-09-02T14:56:34.260Z", 195 | "updatedAt": "2013-09-02T15:22:39.466Z", 196 | "revision": 2, 197 | "locale": "en-US" 198 | }, 199 | "fields": { 200 | "title": "Jake", 201 | "file": { 202 | "url": "//images.contentful.com/cfexampleapi/4hlteQAXS8iS0YCMU6QMWg/2a4d826144f014109364ccf5c891d2dd/jake.png", 203 | "details": { 204 | "size": 20480, 205 | "image": { 206 | "width": 100, 207 | "height": 161 208 | } 209 | }, 210 | "fileName": "jake.png", 211 | "contentType": "image/png" 212 | } 213 | } 214 | } 215 | ] 216 | } 217 | -------------------------------------------------------------------------------- /testdata/spaces-id1-content_types-cat.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "space": { 4 | "sys": { 5 | "type": "Link", 6 | "linkType": "Space", 7 | "id": "cfexampleapi" 8 | } 9 | }, 10 | "id": "cat", 11 | "type": "ContentType", 12 | "createdAt": "2013-06-27T22:46:12.852Z", 13 | "updatedAt": "2016-11-21T15:01:43.860Z", 14 | "revision": 6 15 | }, 16 | "displayField": "name", 17 | "name": "Cat", 18 | "description": "Meow.", 19 | "fields": [ 20 | { 21 | "id": "name", 22 | "name": "Name", 23 | "type": "Text", 24 | "localized": true, 25 | "required": true, 26 | "disabled": false, 27 | "omitted": false 28 | }, 29 | { 30 | "id": "likes", 31 | "name": "Likes", 32 | "type": "Array", 33 | "localized": false, 34 | "required": false, 35 | "disabled": false, 36 | "omitted": false, 37 | "items": { 38 | "type": "Symbol", 39 | "validations": [] 40 | } 41 | }, 42 | { 43 | "id": "color", 44 | "name": "Color", 45 | "type": "Symbol", 46 | "localized": false, 47 | "required": false, 48 | "disabled": false, 49 | "omitted": false 50 | }, 51 | { 52 | "id": "bestFriend", 53 | "name": "Best Friend", 54 | "type": "Link", 55 | "localized": false, 56 | "required": false, 57 | "disabled": false, 58 | "omitted": false, 59 | "linkType": "Entry" 60 | }, 61 | { 62 | "id": "birthday", 63 | "name": "Birthday", 64 | "type": "Date", 65 | "localized": false, 66 | "required": false, 67 | "disabled": false, 68 | "omitted": false 69 | }, 70 | { 71 | "id": "lifes", 72 | "name": "Lifes left", 73 | "type": "Integer", 74 | "localized": false, 75 | "required": false, 76 | "disabled": true, 77 | "omitted": false 78 | }, 79 | { 80 | "id": "lives", 81 | "name": "Lives left", 82 | "type": "Integer", 83 | "localized": false, 84 | "required": false, 85 | "disabled": false, 86 | "omitted": false 87 | }, 88 | { 89 | "id": "image", 90 | "name": "Image", 91 | "type": "Link", 92 | "localized": false, 93 | "required": false, 94 | "disabled": false, 95 | "omitted": false, 96 | "linkType": "Asset" 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /testdata/spaces-id1-content_types-new.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "ContentType", 4 | "id": "3ORKIAOaJqQWWg86MWkyOs", 5 | "space": { 6 | "sys": { 7 | "type": "Link", 8 | "linkType": "Space", 9 | "id": "fp91oelsziea" 10 | } 11 | }, 12 | "version": 1, 13 | "createdAt": "2015-05-18T11:29:46.809Z", 14 | "createdBy": { 15 | "sys": { 16 | "type": "Link", 17 | "linkType": "User", 18 | "id": "4FLrUHftHW3v2BLi9fzfjU" 19 | } 20 | }, 21 | "updatedAt": "2015-05-18T11:29:46.809Z", 22 | "updatedBy": { 23 | "sys": { 24 | "type": "Link", 25 | "linkType": "User", 26 | "id": "4FLrUHftHW3v2BLi9fzfjU" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /testdata/spaces-id1-entries-happycat.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "space": { 4 | "sys": { 5 | "type": "Link", 6 | "linkType": "Space", 7 | "id": "cfexampleapi" 8 | } 9 | }, 10 | "id": "happycat", 11 | "type": "Entry", 12 | "createdAt": "2013-06-27T22:46:20.171Z", 13 | "updatedAt": "2013-11-18T15:58:02.018Z", 14 | "revision": 8, 15 | "contentType": { 16 | "sys": { 17 | "type": "Link", 18 | "linkType": "ContentType", 19 | "id": "cat" 20 | } 21 | }, 22 | "locale": "en-US" 23 | }, 24 | "fields": { 25 | "name": "Happy Cat", 26 | "likes": [ 27 | "cheezburger" 28 | ], 29 | "color": "gray", 30 | "bestFriend": { 31 | "sys": { 32 | "type": "Link", 33 | "linkType": "Entry", 34 | "id": "nyancat" 35 | } 36 | }, 37 | "birthday": "2003-10-28T23:00:00+00:00", 38 | "lives": 1, 39 | "image": { 40 | "sys": { 41 | "type": "Link", 42 | "linkType": "Asset", 43 | "id": "happycat" 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /testdata/spaces-id1-entries-nyancat.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "space": { 4 | "sys": { 5 | "type": "Link", 6 | "linkType": "Space", 7 | "id": "cfexampleapi" 8 | } 9 | }, 10 | "id": "nyancat", 11 | "type": "Entry", 12 | "createdAt": "2013-06-27T22:46:19.513Z", 13 | "updatedAt": "2013-09-04T09:19:39.027Z", 14 | "revision": 5, 15 | "contentType": { 16 | "sys": { 17 | "type": "Link", 18 | "linkType": "ContentType", 19 | "id": "cat" 20 | } 21 | }, 22 | "locale": "en-US" 23 | }, 24 | "fields": { 25 | "name": "Nyan Cat", 26 | "likes": [ 27 | "rainbows", 28 | "fish" 29 | ], 30 | "color": "rainbow", 31 | "bestFriend": { 32 | "sys": { 33 | "type": "Link", 34 | "linkType": "Entry", 35 | "id": "happycat" 36 | } 37 | }, 38 | "birthday": "2011-04-04T22:00:00+00:00", 39 | "lives": 1337, 40 | "image": { 41 | "sys": { 42 | "type": "Link", 43 | "linkType": "Asset", 44 | "id": "nyancat" 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /testdata/spaces-id1-entries.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "total": 10, 6 | "skip": 0, 7 | "limit": 100, 8 | "items": [ 9 | { 10 | "sys": { 11 | "space": { 12 | "sys": { 13 | "type": "Link", 14 | "linkType": "Space", 15 | "id": "cfexampleapi" 16 | } 17 | }, 18 | "id": "happycat", 19 | "type": "Entry", 20 | "createdAt": "2013-06-27T22:46:20.171Z", 21 | "updatedAt": "2013-11-18T15:58:02.018Z", 22 | "revision": 8, 23 | "contentType": { 24 | "sys": { 25 | "type": "Link", 26 | "linkType": "ContentType", 27 | "id": "cat" 28 | } 29 | }, 30 | "locale": "en-US" 31 | }, 32 | "fields": { 33 | "name": "Happy Cat", 34 | "likes": [ 35 | "cheezburger" 36 | ], 37 | "color": "gray", 38 | "bestFriend": { 39 | "sys": { 40 | "type": "Link", 41 | "linkType": "Entry", 42 | "id": "nyancat" 43 | } 44 | }, 45 | "birthday": "2003-10-28T23:00:00+00:00", 46 | "lives": 1, 47 | "image": { 48 | "sys": { 49 | "type": "Link", 50 | "linkType": "Asset", 51 | "id": "happycat" 52 | } 53 | } 54 | } 55 | }, 56 | { 57 | "sys": { 58 | "space": { 59 | "sys": { 60 | "type": "Link", 61 | "linkType": "Space", 62 | "id": "cfexampleapi" 63 | } 64 | }, 65 | "id": "5ETMRzkl9KM4omyMwKAOki", 66 | "type": "Entry", 67 | "createdAt": "2014-02-21T13:42:57.752Z", 68 | "updatedAt": "2014-08-23T14:42:35.207Z", 69 | "revision": 3, 70 | "contentType": { 71 | "sys": { 72 | "type": "Link", 73 | "linkType": "ContentType", 74 | "id": "1t9IbcfdCk6m04uISSsaIK" 75 | } 76 | }, 77 | "locale": "en-US" 78 | }, 79 | "fields": { 80 | "name": "London", 81 | "center": { 82 | "lon": -0.12548719999995228, 83 | "lat": 51.508515 84 | } 85 | } 86 | }, 87 | { 88 | "sys": { 89 | "space": { 90 | "sys": { 91 | "type": "Link", 92 | "linkType": "Space", 93 | "id": "cfexampleapi" 94 | } 95 | }, 96 | "id": "6KntaYXaHSyIw8M6eo26OK", 97 | "type": "Entry", 98 | "createdAt": "2013-11-06T09:45:27.475Z", 99 | "updatedAt": "2013-11-18T09:13:37.808Z", 100 | "revision": 2, 101 | "contentType": { 102 | "sys": { 103 | "type": "Link", 104 | "linkType": "ContentType", 105 | "id": "dog" 106 | } 107 | }, 108 | "locale": "en-US" 109 | }, 110 | "fields": { 111 | "name": "Doge", 112 | "description": "such json\nwow", 113 | "image": { 114 | "sys": { 115 | "type": "Link", 116 | "linkType": "Asset", 117 | "id": "1x0xpXu4pSGS4OukSyWGUK" 118 | } 119 | } 120 | } 121 | }, 122 | { 123 | "sys": { 124 | "space": { 125 | "sys": { 126 | "type": "Link", 127 | "linkType": "Space", 128 | "id": "cfexampleapi" 129 | } 130 | }, 131 | "id": "7qVBlCjpWE86Oseo40gAEY", 132 | "type": "Entry", 133 | "createdAt": "2014-02-21T13:43:38.258Z", 134 | "updatedAt": "2014-04-15T08:22:22.010Z", 135 | "revision": 2, 136 | "contentType": { 137 | "sys": { 138 | "type": "Link", 139 | "linkType": "ContentType", 140 | "id": "1t9IbcfdCk6m04uISSsaIK" 141 | } 142 | }, 143 | "locale": "en-US" 144 | }, 145 | "fields": { 146 | "name": "San Francisco", 147 | "center": { 148 | "lon": -122.41941550000001, 149 | "lat": 37.7749295 150 | } 151 | } 152 | }, 153 | { 154 | "sys": { 155 | "space": { 156 | "sys": { 157 | "type": "Link", 158 | "linkType": "Space", 159 | "id": "cfexampleapi" 160 | } 161 | }, 162 | "id": "garfield", 163 | "type": "Entry", 164 | "createdAt": "2013-06-27T22:46:20.821Z", 165 | "updatedAt": "2013-08-27T10:09:07.929Z", 166 | "revision": 2, 167 | "contentType": { 168 | "sys": { 169 | "type": "Link", 170 | "linkType": "ContentType", 171 | "id": "cat" 172 | } 173 | }, 174 | "locale": "en-US" 175 | }, 176 | "fields": { 177 | "name": "Garfield", 178 | "likes": [ 179 | "lasagna" 180 | ], 181 | "color": "orange", 182 | "birthday": "1979-06-18T23:00:00+00:00", 183 | "lifes": null, 184 | "lives": 9 185 | } 186 | }, 187 | { 188 | "sys": { 189 | "space": { 190 | "sys": { 191 | "type": "Link", 192 | "linkType": "Space", 193 | "id": "cfexampleapi" 194 | } 195 | }, 196 | "id": "4MU1s3potiUEM2G4okYOqw", 197 | "type": "Entry", 198 | "createdAt": "2014-02-21T13:42:45.926Z", 199 | "updatedAt": "2014-02-21T13:42:45.926Z", 200 | "revision": 1, 201 | "contentType": { 202 | "sys": { 203 | "type": "Link", 204 | "linkType": "ContentType", 205 | "id": "1t9IbcfdCk6m04uISSsaIK" 206 | } 207 | }, 208 | "locale": "en-US" 209 | }, 210 | "fields": { 211 | "name": "Berlin", 212 | "center": { 213 | "lon": 13.404953999999975, 214 | "lat": 52.52000659999999 215 | } 216 | } 217 | }, 218 | { 219 | "sys": { 220 | "space": { 221 | "sys": { 222 | "type": "Link", 223 | "linkType": "Space", 224 | "id": "cfexampleapi" 225 | } 226 | }, 227 | "id": "nyancat", 228 | "type": "Entry", 229 | "createdAt": "2013-06-27T22:46:19.513Z", 230 | "updatedAt": "2013-09-04T09:19:39.027Z", 231 | "revision": 5, 232 | "contentType": { 233 | "sys": { 234 | "type": "Link", 235 | "linkType": "ContentType", 236 | "id": "cat" 237 | } 238 | }, 239 | "locale": "en-US" 240 | }, 241 | "fields": { 242 | "name": "Nyan Cat", 243 | "likes": [ 244 | "rainbows", 245 | "fish" 246 | ], 247 | "color": "rainbow", 248 | "bestFriend": { 249 | "sys": { 250 | "type": "Link", 251 | "linkType": "Entry", 252 | "id": "happycat" 253 | } 254 | }, 255 | "birthday": "2011-04-04T22:00:00+00:00", 256 | "lives": 1337, 257 | "image": { 258 | "sys": { 259 | "type": "Link", 260 | "linkType": "Asset", 261 | "id": "nyancat" 262 | } 263 | } 264 | } 265 | }, 266 | { 267 | "sys": { 268 | "space": { 269 | "sys": { 270 | "type": "Link", 271 | "linkType": "Space", 272 | "id": "cfexampleapi" 273 | } 274 | }, 275 | "id": "ge1xHyH3QOWucKWCCAgIG", 276 | "type": "Entry", 277 | "createdAt": "2014-02-21T13:43:23.210Z", 278 | "updatedAt": "2014-02-21T13:43:23.210Z", 279 | "revision": 1, 280 | "contentType": { 281 | "sys": { 282 | "type": "Link", 283 | "linkType": "ContentType", 284 | "id": "1t9IbcfdCk6m04uISSsaIK" 285 | } 286 | }, 287 | "locale": "en-US" 288 | }, 289 | "fields": { 290 | "name": "Paris", 291 | "center": { 292 | "lon": 2.3522219000000177, 293 | "lat": 48.856614 294 | } 295 | } 296 | }, 297 | { 298 | "sys": { 299 | "space": { 300 | "sys": { 301 | "type": "Link", 302 | "linkType": "Space", 303 | "id": "cfexampleapi" 304 | } 305 | }, 306 | "id": "finn", 307 | "type": "Entry", 308 | "createdAt": "2013-06-27T22:46:21.450Z", 309 | "updatedAt": "2013-09-09T16:15:01.297Z", 310 | "revision": 6, 311 | "contentType": { 312 | "sys": { 313 | "type": "Link", 314 | "linkType": "ContentType", 315 | "id": "human" 316 | } 317 | }, 318 | "locale": "en-US" 319 | }, 320 | "fields": { 321 | "name": "Finn", 322 | "description": "Fearless adventurer! Defender of pancakes.", 323 | "likes": [ 324 | "adventure" 325 | ] 326 | } 327 | }, 328 | { 329 | "sys": { 330 | "space": { 331 | "sys": { 332 | "type": "Link", 333 | "linkType": "Space", 334 | "id": "cfexampleapi" 335 | } 336 | }, 337 | "id": "jake", 338 | "type": "Entry", 339 | "createdAt": "2013-06-27T22:46:22.096Z", 340 | "updatedAt": "2013-12-18T13:10:26.212Z", 341 | "revision": 5, 342 | "contentType": { 343 | "sys": { 344 | "type": "Link", 345 | "linkType": "ContentType", 346 | "id": "dog" 347 | } 348 | }, 349 | "locale": "en-US" 350 | }, 351 | "fields": { 352 | "name": "Jake", 353 | "description": "Bacon pancakes, makin' bacon pancakes!", 354 | "image": { 355 | "sys": { 356 | "type": "Link", 357 | "linkType": "Asset", 358 | "id": "jake" 359 | } 360 | } 361 | } 362 | } 363 | ], 364 | "includes": { 365 | "Asset": [ 366 | { 367 | "sys": { 368 | "space": { 369 | "sys": { 370 | "type": "Link", 371 | "linkType": "Space", 372 | "id": "cfexampleapi" 373 | } 374 | }, 375 | "id": "1x0xpXu4pSGS4OukSyWGUK", 376 | "type": "Asset", 377 | "createdAt": "2013-11-06T09:45:10.000Z", 378 | "updatedAt": "2013-12-18T13:27:14.917Z", 379 | "revision": 6, 380 | "locale": "en-US" 381 | }, 382 | "fields": { 383 | "title": "Doge", 384 | "description": "nice picture", 385 | "file": { 386 | "url": "//images.contentful.com/cfexampleapi/1x0xpXu4pSGS4OukSyWGUK/cc1239c6385428ef26f4180190532818/doge.jpg", 387 | "details": { 388 | "size": 522943, 389 | "image": { 390 | "width": 5800, 391 | "height": 4350 392 | } 393 | }, 394 | "fileName": "doge.jpg", 395 | "contentType": "image/jpeg" 396 | } 397 | } 398 | }, 399 | { 400 | "sys": { 401 | "space": { 402 | "sys": { 403 | "type": "Link", 404 | "linkType": "Space", 405 | "id": "cfexampleapi" 406 | } 407 | }, 408 | "id": "happycat", 409 | "type": "Asset", 410 | "createdAt": "2013-09-02T14:56:34.267Z", 411 | "updatedAt": "2013-09-02T15:11:24.361Z", 412 | "revision": 2, 413 | "locale": "en-US" 414 | }, 415 | "fields": { 416 | "title": "Happy Cat", 417 | "file": { 418 | "url": "//images.contentful.com/cfexampleapi/3MZPnjZTIskAIIkuuosCss/382a48dfa2cb16c47aa2c72f7b23bf09/happycatw.jpg", 419 | "details": { 420 | "size": 59939, 421 | "image": { 422 | "width": 273, 423 | "height": 397 424 | } 425 | }, 426 | "fileName": "happycatw.jpg", 427 | "contentType": "image/jpeg" 428 | } 429 | } 430 | }, 431 | { 432 | "sys": { 433 | "space": { 434 | "sys": { 435 | "type": "Link", 436 | "linkType": "Space", 437 | "id": "cfexampleapi" 438 | } 439 | }, 440 | "id": "jake", 441 | "type": "Asset", 442 | "createdAt": "2013-09-02T14:56:34.260Z", 443 | "updatedAt": "2013-09-02T15:22:39.466Z", 444 | "revision": 2, 445 | "locale": "en-US" 446 | }, 447 | "fields": { 448 | "title": "Jake", 449 | "file": { 450 | "url": "//images.contentful.com/cfexampleapi/4hlteQAXS8iS0YCMU6QMWg/2a4d826144f014109364ccf5c891d2dd/jake.png", 451 | "details": { 452 | "size": 20480, 453 | "image": { 454 | "width": 100, 455 | "height": 161 456 | } 457 | }, 458 | "fileName": "jake.png", 459 | "contentType": "image/png" 460 | } 461 | } 462 | }, 463 | { 464 | "sys": { 465 | "space": { 466 | "sys": { 467 | "type": "Link", 468 | "linkType": "Space", 469 | "id": "cfexampleapi" 470 | } 471 | }, 472 | "id": "nyancat", 473 | "type": "Asset", 474 | "createdAt": "2013-09-02T14:56:34.240Z", 475 | "updatedAt": "2013-09-02T14:56:34.240Z", 476 | "revision": 1, 477 | "locale": "en-US" 478 | }, 479 | "fields": { 480 | "title": "Nyan Cat", 481 | "file": { 482 | "url": "//images.contentful.com/cfexampleapi/4gp6taAwW4CmSgumq2ekUm/9da0cd1936871b8d72343e895a00d611/Nyan_cat_250px_frame.png", 483 | "details": { 484 | "size": 12273, 485 | "image": { 486 | "width": 250, 487 | "height": 250 488 | } 489 | }, 490 | "fileName": "Nyan_cat_250px_frame.png", 491 | "contentType": "image/png" 492 | } 493 | } 494 | } 495 | ] 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /testdata/spaces-id1.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Space", 4 | "id": "id1", 5 | "createdAt": "somedate", 6 | "version": 3 7 | }, 8 | "name": "Contentful Example API", 9 | "locales": [ 10 | { 11 | "code": "en-US", 12 | "default": true, 13 | "name": "English", 14 | "fallbackCode": null 15 | }, 16 | { 17 | "code": "tlh", 18 | "default": false, 19 | "name": "Klingon", 20 | "fallbackCode": "en-US" 21 | }, 22 | { 23 | "code": "bc-BC", 24 | "default": false, 25 | "name": "Baconglish", 26 | "fallbackCode": "tlh" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /testdata/spaces-id2.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Space", 4 | "id": "id2" 5 | }, 6 | "name": "Contentful Example API", 7 | "locales": [ 8 | { 9 | "code": "en-US", 10 | "default": true, 11 | "name": "English", 12 | "fallbackCode": null 13 | }, 14 | { 15 | "code": "tlh", 16 | "default": false, 17 | "name": "Klingon", 18 | "fallbackCode": "en-US" 19 | } 20 | ] 21 | }, 22 | -------------------------------------------------------------------------------- /testdata/spaces-newspace-updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "changed-space-name", 3 | "sys": { 4 | "type": "Space", 5 | "id": "newspace", 6 | "version": 2, 7 | "createdBy": { 8 | "sys": { 9 | "type": "Link", 10 | "linkType": "User", 11 | "id": "50ZPikUCLbyl9V6zQZNWSL" 12 | } 13 | }, 14 | "createdAt": "2017-03-12T13:08:46Z", 15 | "updatedBy": { 16 | "sys": { 17 | "type": "Link", 18 | "linkType": "User", 19 | "id": "50ZPikUCLbyl9V6zQZNWSL" 20 | } 21 | }, 22 | "updatedAt": "2017-03-12T13:08:46Z" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/spaces-newspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new space", 3 | "sys": { 4 | "type": "Space", 5 | "id": "newspace", 6 | "version": 1, 7 | "createdBy": { 8 | "sys": { 9 | "type": "Link", 10 | "linkType": "User", 11 | "id": "50ZPikUCLbyl9V6zQZNWSL" 12 | } 13 | }, 14 | "createdAt": "2017-03-12T13:08:46Z", 15 | "updatedBy": { 16 | "sys": { 17 | "type": "Link", 18 | "linkType": "User", 19 | "id": "50ZPikUCLbyl9V6zQZNWSL" 20 | } 21 | }, 22 | "updatedAt": "2017-03-12T13:08:46Z" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/spaces-page-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "skip": 0, 6 | "limit": 100, 7 | "total": 2, 8 | "items": [] 9 | } 10 | -------------------------------------------------------------------------------- /testdata/spaces.json: -------------------------------------------------------------------------------- 1 | { 2 | "sys": { 3 | "type": "Array" 4 | }, 5 | "skip": 0, 6 | "limit": 100, 7 | "total": 2, 8 | "items": [ 9 | { 10 | "sys": { 11 | "type": "Space", 12 | "id": "id1" 13 | }, 14 | "name": "Contentful Example API", 15 | "locales": [ 16 | { 17 | "code": "en-US", 18 | "default": true, 19 | "name": "English", 20 | "fallbackCode": null 21 | }, 22 | { 23 | "code": "tlh", 24 | "default": false, 25 | "name": "Klingon", 26 | "fallbackCode": "en-US" 27 | } 28 | ] 29 | }, 30 | { 31 | "sys": { 32 | "type": "Space", 33 | "id": "id2" 34 | }, 35 | "name": "Contentful Example API", 36 | "locales": [ 37 | { 38 | "code": "en-US", 39 | "default": true, 40 | "name": "English", 41 | "fallbackCode": null 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /testdata/webhook-updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://www.example.com/test-updated", 3 | "httpBasicUsername": "updated-username", 4 | "name": "updated-webhook-name", 5 | "headers": [ 6 | { 7 | "key": "header1", 8 | "value": "updated-header1-value" 9 | }, 10 | { 11 | "key": "header2", 12 | "value": "updated-header2-value" 13 | } 14 | ], 15 | "topics": [ 16 | "Entry.create", 17 | "ContentType.create", 18 | "Asset.create" 19 | ], 20 | "sys": { 21 | "type": "WebhookDefinition", 22 | "id": "7fstd9fZ9T2p3kwD49FxhI", 23 | "version": 1, 24 | "space": { 25 | "sys": { 26 | "type": "Link", 27 | "linkType": "Space", 28 | "id": "q65ipbk62rgw" 29 | } 30 | }, 31 | "createdBy": { 32 | "sys": { 33 | "type": "Link", 34 | "linkType": "User", 35 | "id": "7aAReMWo8woRiCCd2wFNwB" 36 | } 37 | }, 38 | "createdAt": "2017-03-20T17:52:38Z", 39 | "updatedBy": { 40 | "sys": { 41 | "type": "Link", 42 | "linkType": "User", 43 | "id": "7aAReMWo8woRiCCd2wFNwB" 44 | } 45 | }, 46 | "updatedAt": "2017-03-20T17:52:38Z" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /testdata/webhook.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://www.example.com/test", 3 | "httpBasicUsername": "username", 4 | "name": "webhook-name", 5 | "headers": [ 6 | { 7 | "key": "header1", 8 | "value": "header1-value" 9 | }, 10 | { 11 | "key": "header2", 12 | "value": "header2-value" 13 | } 14 | ], 15 | "topics": [ 16 | "Entry.create", 17 | "ContentType.create" 18 | ], 19 | "sys": { 20 | "type": "WebhookDefinition", 21 | "id": "7fstd9fZ9T2p3kwD49FxhI", 22 | "version": 0, 23 | "space": { 24 | "sys": { 25 | "type": "Link", 26 | "linkType": "Space", 27 | "id": "q65ipbk62rgw" 28 | } 29 | }, 30 | "createdBy": { 31 | "sys": { 32 | "type": "Link", 33 | "linkType": "User", 34 | "id": "7aAReMWo8woRiCCd2wFNwB" 35 | } 36 | }, 37 | "createdAt": "2017-03-20T17:52:38Z", 38 | "updatedBy": { 39 | "sys": { 40 | "type": "Link", 41 | "linkType": "User", 42 | "id": "7aAReMWo8woRiCCd2wFNwB" 43 | } 44 | }, 45 | "updatedAt": "2017-03-20T17:52:38Z" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tools/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | go get -u github.com/golang/lint/golint 5 | 6 | for d in $(go list ./... | grep -v /vendor/); do 7 | res=$(golint -min_confidence 0.8 $d) 8 | 9 | if [[ $res != '' ]]; then 10 | echo "$res" 11 | exit 1 12 | fi 13 | done 14 | -------------------------------------------------------------------------------- /tools/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | rm coverage.txt || true 5 | touch coverage.txt 6 | 7 | for d in $(go list ./... | grep -v /vendor/); do 8 | go test -v -coverprofile=profile.out -covermode=count $d 9 | 10 | if [ -f profile.out ]; then 11 | cat profile.out >> coverage.txt 12 | rm profile.out 13 | fi 14 | done 15 | 16 | # to make `go tool cover -html=coverage.txt` happy 17 | # remove the lines starting with mode 18 | # remove the empty lines 19 | sed -i'' -e '/^\s*$/d' coverage.txt 20 | echo "$(awk '!/^mode:/ || !f++' coverage.txt)" > coverage.txt 21 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | // Sys model 4 | type Sys struct { 5 | ID string `json:"id,omitempty"` 6 | Type string `json:"type,omitempty"` 7 | LinkType string `json:"linkType,omitempty"` 8 | CreatedAt string `json:"createdAt,omitempty"` 9 | UpdatedAt string `json:"updatedAt,omitempty"` 10 | UpdatedBy *Sys `json:"updatedBy,omitempty"` 11 | Version int `json:"version,omitempty"` 12 | Revision int `json:"revision,omitempty"` 13 | ContentType *ContentType `json:"contentType,omitempty"` 14 | Space *Space `json:"space,omitempty"` 15 | FirstPublishedAt string `json:"firstPublishedAt,omitempty"` 16 | PublishedCounter int `json:"publishedCounter,omitempty"` 17 | PublishedAt string `json:"publishedAt,omitempty"` 18 | PublishedBy *Sys `json:"publishedBy,omitempty"` 19 | PublishedVersion int `json:"publishedVersion,omitempty"` 20 | } 21 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | // Version for SDK Version 4 | var Version = "0.3.1" 5 | -------------------------------------------------------------------------------- /webhook.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "strconv" 8 | ) 9 | 10 | // WebhooksService service 11 | type WebhooksService service 12 | 13 | // Webhook model 14 | type Webhook struct { 15 | Sys *Sys `json:"sys,omitempty"` 16 | Name string `json:"name,omitempty"` 17 | URL string `json:"url,omitempty"` 18 | Topics []string `json:"topics,omitempty"` 19 | HTTPBasicUsername string `json:"httpBasicUsername,omitempty"` 20 | HTTPBasicPassword string `json:"httpBasicPassword,omitempty"` 21 | Headers []*WebhookHeader `json:"headers,omitempty"` 22 | } 23 | 24 | // WebhookHeader model 25 | type WebhookHeader struct { 26 | Key string `json:"key"` 27 | Value string `json:"value"` 28 | } 29 | 30 | // GetVersion returns entity version 31 | func (webhook *Webhook) GetVersion() int { 32 | version := 1 33 | if webhook.Sys != nil { 34 | version = webhook.Sys.Version 35 | } 36 | 37 | return version 38 | } 39 | 40 | // List returns webhooks collection 41 | func (service *WebhooksService) List(spaceID string) *Collection { 42 | path := fmt.Sprintf("/spaces/%s/webhook_definitions", spaceID) 43 | method := "GET" 44 | 45 | req, err := service.c.newRequest(method, path, nil, nil) 46 | if err != nil { 47 | return &Collection{} 48 | } 49 | 50 | col := NewCollection(&CollectionOptions{}) 51 | col.c = service.c 52 | col.req = req 53 | 54 | return col 55 | } 56 | 57 | // Get returns a single webhook entity 58 | func (service *WebhooksService) Get(spaceID, webhookID string) (*Webhook, error) { 59 | path := fmt.Sprintf("/spaces/%s/webhook_definitions/%s", spaceID, webhookID) 60 | method := "GET" 61 | 62 | req, err := service.c.newRequest(method, path, nil, nil) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | var webhook Webhook 68 | if err := service.c.do(req, &webhook); err != nil { 69 | return nil, err 70 | } 71 | 72 | return &webhook, nil 73 | } 74 | 75 | // Upsert updates or creates a new entity 76 | func (service *WebhooksService) Upsert(spaceID string, webhook *Webhook) error { 77 | bytesArray, err := json.Marshal(webhook) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | var path string 83 | var method string 84 | 85 | if webhook.Sys != nil && webhook.Sys.CreatedAt != "" { 86 | path = fmt.Sprintf("/spaces/%s/webhook_definitions/%s", spaceID, webhook.Sys.ID) 87 | method = "PUT" 88 | } else { 89 | path = fmt.Sprintf("/spaces/%s/webhook_definitions", spaceID) 90 | method = "POST" 91 | } 92 | 93 | req, err := service.c.newRequest(method, path, nil, bytes.NewReader(bytesArray)) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | req.Header.Set("X-Contentful-Version", strconv.Itoa(webhook.GetVersion())) 99 | 100 | return service.c.do(req, webhook) 101 | } 102 | 103 | // Delete the webhook 104 | func (service *WebhooksService) Delete(spaceID string, webhook *Webhook) error { 105 | path := fmt.Sprintf("/spaces/%s/webhook_definitions/%s", spaceID, webhook.Sys.ID) 106 | method := "DELETE" 107 | 108 | req, err := service.c.newRequest(method, path, nil, nil) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | version := strconv.Itoa(webhook.Sys.Version) 114 | req.Header.Set("X-Contentful-Version", version) 115 | 116 | return service.c.do(req, nil) 117 | } 118 | -------------------------------------------------------------------------------- /webhook_test.go: -------------------------------------------------------------------------------- 1 | package contentful 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestWebhookSaveForCreate(t *testing.T) { 14 | var err error 15 | assert := assert.New(t) 16 | 17 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | assert.Equal(r.Method, "POST") 19 | assert.Equal(r.RequestURI, "/spaces/"+spaceID+"/webhook_definitions") 20 | checkHeaders(r, assert) 21 | 22 | var payload map[string]interface{} 23 | err := json.NewDecoder(r.Body).Decode(&payload) 24 | assert.Nil(err) 25 | assert.Equal("webhook-name", payload["name"]) 26 | assert.Equal("https://www.example.com/test", payload["url"]) 27 | assert.Equal("username", payload["httpBasicUsername"]) 28 | assert.Equal("password", payload["httpBasicPassword"]) 29 | 30 | topics := payload["topics"].([]interface{}) 31 | assert.Equal(2, len(topics)) 32 | assert.Equal("Entry.create", topics[0].(string)) 33 | assert.Equal("ContentType.create", topics[1].(string)) 34 | 35 | headers := payload["headers"].([]interface{}) 36 | assert.Equal(2, len(headers)) 37 | header1 := headers[0].(map[string]interface{}) 38 | header2 := headers[1].(map[string]interface{}) 39 | 40 | assert.Equal("header1", header1["key"].(string)) 41 | assert.Equal("header1-value", header1["value"].(string)) 42 | 43 | assert.Equal("header2", header2["key"].(string)) 44 | assert.Equal("header2-value", header2["value"].(string)) 45 | 46 | w.WriteHeader(201) 47 | fmt.Fprintln(w, string(readTestData("webhook.json"))) 48 | }) 49 | 50 | // test server 51 | server := httptest.NewServer(handler) 52 | defer server.Close() 53 | 54 | // cma client 55 | cma = NewCMA(CMAToken) 56 | cma.BaseURL = server.URL 57 | 58 | webhook := &Webhook{ 59 | Name: "webhook-name", 60 | URL: "https://www.example.com/test", 61 | Topics: []string{ 62 | "Entry.create", 63 | "ContentType.create", 64 | }, 65 | HTTPBasicUsername: "username", 66 | HTTPBasicPassword: "password", 67 | Headers: []*WebhookHeader{ 68 | &WebhookHeader{ 69 | Key: "header1", 70 | Value: "header1-value", 71 | }, 72 | &WebhookHeader{ 73 | Key: "header2", 74 | Value: "header2-value", 75 | }, 76 | }, 77 | } 78 | 79 | err = cma.Webhooks.Upsert(spaceID, webhook) 80 | assert.Nil(err) 81 | assert.Equal("7fstd9fZ9T2p3kwD49FxhI", webhook.Sys.ID) 82 | assert.Equal("webhook-name", webhook.Name) 83 | assert.Equal("username", webhook.HTTPBasicUsername) 84 | } 85 | 86 | func TestWebhookSaveForUpdate(t *testing.T) { 87 | var err error 88 | assert := assert.New(t) 89 | 90 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 91 | assert.Equal(r.Method, "PUT") 92 | assert.Equal(r.RequestURI, "/spaces/"+spaceID+"/webhook_definitions/7fstd9fZ9T2p3kwD49FxhI") 93 | checkHeaders(r, assert) 94 | 95 | var payload map[string]interface{} 96 | err := json.NewDecoder(r.Body).Decode(&payload) 97 | assert.Nil(err) 98 | assert.Equal("updated-webhook-name", payload["name"]) 99 | assert.Equal("https://www.example.com/test-updated", payload["url"]) 100 | assert.Equal("updated-username", payload["httpBasicUsername"]) 101 | assert.Equal("updated-password", payload["httpBasicPassword"]) 102 | 103 | topics := payload["topics"].([]interface{}) 104 | assert.Equal(3, len(topics)) 105 | assert.Equal("Entry.create", topics[0].(string)) 106 | assert.Equal("ContentType.create", topics[1].(string)) 107 | assert.Equal("Asset.create", topics[2].(string)) 108 | 109 | headers := payload["headers"].([]interface{}) 110 | assert.Equal(2, len(headers)) 111 | header1 := headers[0].(map[string]interface{}) 112 | header2 := headers[1].(map[string]interface{}) 113 | 114 | assert.Equal("header1", header1["key"].(string)) 115 | assert.Equal("updated-header1-value", header1["value"].(string)) 116 | 117 | assert.Equal("header2", header2["key"].(string)) 118 | assert.Equal("updated-header2-value", header2["value"].(string)) 119 | 120 | w.WriteHeader(200) 121 | fmt.Fprintln(w, string(readTestData("webhook-updated.json"))) 122 | }) 123 | 124 | // test server 125 | server := httptest.NewServer(handler) 126 | defer server.Close() 127 | 128 | // cma client 129 | cma = NewCMA(CMAToken) 130 | cma.BaseURL = server.URL 131 | 132 | // test webhook 133 | webhook, err := webhookFromTestData("webhook.json") 134 | assert.Nil(err) 135 | 136 | webhook.Name = "updated-webhook-name" 137 | webhook.URL = "https://www.example.com/test-updated" 138 | webhook.Topics = []string{ 139 | "Entry.create", 140 | "ContentType.create", 141 | "Asset.create", 142 | } 143 | webhook.HTTPBasicUsername = "updated-username" 144 | webhook.HTTPBasicPassword = "updated-password" 145 | webhook.Headers = []*WebhookHeader{ 146 | &WebhookHeader{ 147 | Key: "header1", 148 | Value: "updated-header1-value", 149 | }, 150 | &WebhookHeader{ 151 | Key: "header2", 152 | Value: "updated-header2-value", 153 | }, 154 | } 155 | 156 | err = cma.Webhooks.Upsert(spaceID, webhook) 157 | assert.Nil(err) 158 | assert.Equal("7fstd9fZ9T2p3kwD49FxhI", webhook.Sys.ID) 159 | assert.Equal(1, webhook.Sys.Version) 160 | assert.Equal("updated-webhook-name", webhook.Name) 161 | assert.Equal("updated-username", webhook.HTTPBasicUsername) 162 | } 163 | 164 | func TestWebhookDelete(t *testing.T) { 165 | var err error 166 | assert := assert.New(t) 167 | 168 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 169 | assert.Equal(r.Method, "DELETE") 170 | assert.Equal(r.RequestURI, "/spaces/"+spaceID+"/webhook_definitions/7fstd9fZ9T2p3kwD49FxhI") 171 | checkHeaders(r, assert) 172 | 173 | w.WriteHeader(200) 174 | }) 175 | 176 | // test server 177 | server := httptest.NewServer(handler) 178 | defer server.Close() 179 | 180 | // cma client 181 | cma = NewCMA(CMAToken) 182 | cma.BaseURL = server.URL 183 | 184 | // test webhook 185 | webhook, err := webhookFromTestData("webhook.json") 186 | assert.Nil(err) 187 | 188 | err = cma.Webhooks.Delete(spaceID, webhook) 189 | assert.Nil(err) 190 | } 191 | --------------------------------------------------------------------------------