├── TODO ├── unsplash ├── endpoints_test.go ├── currentUser_test.go ├── stats_test.go ├── response_test.go ├── user_test.go ├── request_test.go ├── URL.go ├── search_results.go ├── api_scopes.go ├── request.go ├── stats_opt.go ├── list_options_test.go ├── collection.go ├── get_helpers.go ├── list_options.go ├── errors_test.go ├── URL_test.go ├── endpoints.go ├── errors.go ├── doc.go ├── search_service.go ├── search_service_test.go ├── response.go ├── users_service.go ├── photo.go ├── unsplash.go ├── user.go ├── users_service_test.go ├── collections_service.go ├── stats.go ├── unsplash_test.go ├── photos_service.go ├── photos_service_test.go └── collections_service_test.go ├── go.mod ├── .github └── workflows │ └── test.yaml ├── .gitignore ├── LICENSE ├── go.sum ├── CODE_OF_CONDUCT.md └── README.md /TODO: -------------------------------------------------------------------------------- 1 | -Search service (Not returning link headers) 2 | -upload a photo- is this endpoint available? 3 | Unsplash official libraries have this; study and then test it on postman first. 4 | -update a photo 5 | -------------------------------------------------------------------------------- /unsplash/endpoints_test.go: -------------------------------------------------------------------------------- 1 | package unsplash 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestUrlChanged(T *testing.T) { 9 | assert := assert.New(T) 10 | 11 | var baseUrl = "https://example.com" 12 | SetupBaseUrl(baseUrl) 13 | 14 | assert.Equal(baseUrl, getEndpoint(base)) 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hbagdi/go-unsplash 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/google/go-querystring v1.1.0 7 | github.com/jarcoal/httpmock v1.0.4 8 | github.com/stretchr/testify v1.3.0 9 | golang.org/x/oauth2 v0.27.0 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.0 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: CI Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | env: 8 | UNSPLASH_USERTOKEN: ${{ secrets.UNSPLASH_USERTOKEN }} 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Setup go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: '^1.16' 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | - name: go test 18 | run: go test -v ./... 19 | -------------------------------------------------------------------------------- /.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 | vendor 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | 27 | # for this repo only 28 | coverage.txt 29 | auth.env 30 | *.json 31 | 32 | unsplash/out 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2017 Hardik Bagdi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /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/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 4 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 5 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 6 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 7 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 8 | github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA= 9 | github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 14 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 15 | golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= 16 | golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 17 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 18 | -------------------------------------------------------------------------------- /unsplash/currentUser_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "testing" 28 | 29 | "github.com/stretchr/testify/assert" 30 | ) 31 | 32 | func TestCurrentUser(T *testing.T) { 33 | assert := assert.New(T) 34 | unsplash := setup() 35 | 36 | user, resp, err := unsplash.CurrentUser() 37 | assert.NotNil(user) 38 | assert.NotNil(resp) 39 | assert.Nil(err) 40 | } 41 | -------------------------------------------------------------------------------- /unsplash/stats_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "io/ioutil" 28 | "log" 29 | "testing" 30 | 31 | "github.com/stretchr/testify/assert" 32 | ) 33 | 34 | func TestGlobalStats(T *testing.T) { 35 | log.SetOutput(ioutil.Discard) 36 | assert := assert.New(T) 37 | unsplash := setup() 38 | 39 | stats, resp, err := unsplash.Stats() 40 | assert.NotNil(stats) 41 | assert.NotNil(resp) 42 | assert.Nil(err) 43 | log.Println(stats) 44 | } 45 | -------------------------------------------------------------------------------- /unsplash/response_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "io/ioutil" 28 | "log" 29 | "testing" 30 | 31 | "github.com/stretchr/testify/assert" 32 | ) 33 | 34 | func TestResponse(T *testing.T) { 35 | assert := assert.New(T) 36 | log.SetOutput(ioutil.Discard) 37 | 38 | response, err := newResponse(nil) 39 | assert.NotNil(err) 40 | assert.Nil(response) 41 | iae, ok := err.(*IllegalArgumentError) 42 | assert.NotNil(iae) 43 | assert.Equal(true, ok) 44 | } 45 | -------------------------------------------------------------------------------- /unsplash/user_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "io/ioutil" 28 | "log" 29 | "testing" 30 | ) 31 | 32 | func TestUserString(T *testing.T) { 33 | log.SetOutput(ioutil.Discard) 34 | u := new(User) 35 | log.Println(u.String()) 36 | } 37 | 38 | func TestUserUpdateInforString(T *testing.T) { 39 | log.SetOutput(ioutil.Discard) 40 | u := new(UserUpdateInfo) 41 | u.Username = "gopher" 42 | u.FirstName = "Go" 43 | u.LastName = "Gopher" 44 | u.Bio = "I'm a gopher." 45 | u.Email = "gopher@gopher.gopher" 46 | u.PortfolioURL = "gopher.com" 47 | u.Location = "1AU" 48 | u.InstagramUsername = "gopher" 49 | log.Println(u.String()) 50 | } 51 | -------------------------------------------------------------------------------- /unsplash/request_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "io/ioutil" 28 | "log" 29 | "testing" 30 | 31 | "github.com/stretchr/testify/assert" 32 | ) 33 | 34 | func TestRequest(T *testing.T) { 35 | assert := assert.New(T) 36 | log.SetOutput(ioutil.Discard) 37 | 38 | request, err := newRequest(GET, "", nil, nil) 39 | assert.NotNil(err) 40 | assert.Nil(request) 41 | iae, ok := err.(*IllegalArgumentError) 42 | assert.NotNil(iae) 43 | assert.Equal(true, ok) 44 | } 45 | 46 | func TestRequestRogue(T *testing.T) { 47 | assert := assert.New(T) 48 | log.SetOutput(ioutil.Discard) 49 | var ROGUE method 50 | ROGUE = "ROGUE" 51 | req, err := newRequest(ROGUE, "", nil, nil) 52 | assert.Nil(req) 53 | assert.NotNil(err) 54 | } 55 | -------------------------------------------------------------------------------- /unsplash/URL.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "encoding/json" 28 | "net/url" 29 | "strings" 30 | ) 31 | 32 | // URL is URL with custom JSON marshalling 33 | type URL struct { 34 | *url.URL 35 | } 36 | 37 | // MarshalJSON marshals the URL object into json string 38 | func (u *URL) MarshalJSON() ([]byte, error) { 39 | return json.Marshal(u.String()) 40 | } 41 | 42 | // UnmarshalJSON takes a string and returns a URL struct 43 | func (u *URL) UnmarshalJSON(b []byte) error { 44 | var urlString string 45 | err := json.Unmarshal(b, &urlString) 46 | if err != nil { 47 | return err 48 | } 49 | parsedURL, err := url.Parse(strings.TrimSpace(urlString)) 50 | if err != nil { 51 | return err 52 | } 53 | u.URL = parsedURL 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /unsplash/search_results.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | // UserSearchResult represnts the result for a search for users. 27 | type UserSearchResult struct { 28 | Total *int `json:"total"` 29 | TotalPages *int `json:"total_pages"` 30 | Results *[]User `json:"results"` 31 | } 32 | 33 | // PhotoSearchResult represnts the result for a search for photos. 34 | type PhotoSearchResult struct { 35 | Total *int `json:"total"` 36 | TotalPages *int `json:"total_pages"` 37 | Results *[]Photo `json:"results"` 38 | } 39 | 40 | // CollectionSearchResult represnts the result for a search for collections. 41 | type CollectionSearchResult struct { 42 | Total *int `json:"total"` 43 | TotalPages *int `json:"total_pages"` 44 | Results *[]Collection `json:"results"` 45 | } 46 | -------------------------------------------------------------------------------- /unsplash/api_scopes.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | //These are permission scopes for the Unsplash API for OAuth. 27 | const ( 28 | //Public is default; gives access to read public data. 29 | Public = "public" 30 | //ReadUser gives access to read user’s private data. 31 | ReadUser = "read_user" 32 | //WriteUser gives access to update the user’s profile. 33 | WriteUser = "write_user" 34 | //ReadPhotos gives acess to read private data from the user’s photos. 35 | ReadPhotos = "read_photos" 36 | //WritePhotos gives access to update photos on the user’s behalf. 37 | WritePhotos = "write_photos" 38 | //WriteLikes gives access to like/unlike a photo on the user’s behalf. 39 | WriteLikes = "write_likes" 40 | //WriteFollowers gives access to follow or unfollow a user on the user’s behalf. 41 | WriteFollowers = "write_followers" 42 | //ReadCollections gives access to view a user’s private collections. 43 | ReadCollections = "read_collections" 44 | //WriteCollections gives access to create and update a users's collections. 45 | WriteCollections = "write_collections" 46 | ) 47 | -------------------------------------------------------------------------------- /unsplash/request.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "bytes" 28 | "encoding/json" 29 | "net/http" 30 | 31 | "github.com/google/go-querystring/query" 32 | ) 33 | 34 | type request struct { 35 | Request *http.Request 36 | } 37 | 38 | func newRequest(m method, e string, qs interface{}, body interface{}) (*request, error) { 39 | if e == "" { 40 | return nil, &IllegalArgumentError{ErrString: "Endpoint can't be null."} 41 | } 42 | //body to be sent in JSON 43 | buf, err := json.Marshal(body) 44 | if err != nil { 45 | return nil, err 46 | } 47 | //Create a new request 48 | 49 | httpRequest, err := http.NewRequest(string(m), getEndpoint(base)+e, bytes.NewBuffer(buf)) 50 | 51 | if err != nil { 52 | return nil, err 53 | } 54 | //Add query string if any 55 | if qs != nil { 56 | values, err := query.Values(qs) 57 | if err != nil { 58 | return nil, err 59 | } 60 | httpRequest.URL.RawQuery = values.Encode() 61 | } 62 | req := new(request) 63 | req.Request = httpRequest 64 | req.Request.Header.Add("Content-Type", "application/json") 65 | return req, nil 66 | } 67 | -------------------------------------------------------------------------------- /unsplash/stats_opt.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "bytes" 28 | "strconv" 29 | ) 30 | 31 | // StatsOpt is used for custom statistics range 32 | type StatsOpt struct { 33 | Resolution string `url:"resolution"` 34 | Quantity int `url:"quantity"` 35 | } 36 | 37 | var defaultStatsOpt = &StatsOpt{ 38 | Resolution: "days", 39 | Quantity: 30, 40 | } 41 | 42 | // Valid validates the values in a StatsOpt 43 | func (opt *StatsOpt) Valid() bool { 44 | if opt.Quantity < 0 || opt.Quantity > 30 { 45 | return false 46 | } 47 | if opt.Quantity == 0 { 48 | opt.Quantity = 30 49 | } 50 | if opt.Resolution == "" { 51 | opt.Resolution = "days" 52 | } 53 | if opt.Resolution != "days" { 54 | return false 55 | } 56 | return true 57 | } 58 | 59 | func (opt *StatsOpt) String() string { 60 | var buf bytes.Buffer 61 | if !opt.Valid() { 62 | buf.WriteString("StatsOpt is invalid") 63 | } 64 | buf.WriteString("StatsOpt: Resolution[") 65 | buf.WriteString(opt.Resolution) 66 | buf.WriteString("], Quantity[") 67 | buf.WriteString(strconv.Itoa(opt.Quantity)) 68 | buf.WriteString("]") 69 | return buf.String() 70 | } 71 | -------------------------------------------------------------------------------- /unsplash/list_options_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "io/ioutil" 28 | "log" 29 | "testing" 30 | 31 | "github.com/stretchr/testify/assert" 32 | ) 33 | 34 | func TestListOpt(T *testing.T) { 35 | assert := assert.New(T) 36 | log.SetOutput(ioutil.Discard) 37 | 38 | var opt ListOpt 39 | assert.Equal(true, opt.Valid()) 40 | assert.Equal(1, opt.Page) 41 | assert.Equal(10, opt.PerPage) 42 | assert.Equal(Latest, opt.OrderBy) 43 | opt = *defaultListOpt 44 | assert.Equal(true, opt.Valid()) 45 | 46 | opt = ListOpt{OrderBy: Popular} 47 | assert.Equal(true, opt.Valid()) 48 | assert.Equal(1, opt.Page) 49 | assert.Equal(10, opt.PerPage) 50 | 51 | opt = ListOpt{Page: 10} 52 | assert.Equal(true, opt.Valid()) 53 | assert.Equal(Latest, opt.OrderBy) 54 | assert.Equal(10, opt.Page) 55 | assert.Equal(10, opt.PerPage) 56 | 57 | opt = ListOpt{PerPage: 20, OrderBy: Oldest} 58 | assert.Equal(true, opt.Valid()) 59 | assert.Equal(Oldest, opt.OrderBy) 60 | assert.Equal(1, opt.Page) 61 | assert.Equal(20, opt.PerPage) 62 | log.Println(opt.String()) 63 | 64 | opt.OrderBy = "Duck" 65 | assert.Equal(false, opt.Valid()) 66 | } 67 | -------------------------------------------------------------------------------- /unsplash/collection.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "bytes" 28 | "strconv" 29 | ) 30 | 31 | // Collection holds a collection on unsplash.com 32 | type Collection struct { 33 | ID *int `json:"id"` 34 | Title *string `json:"title"` 35 | Description *string `json:"description"` 36 | PublishedAt *string `json:"published_at"` 37 | Curated *bool `json:"curated"` 38 | Featured *bool `json:"featured"` 39 | TotalPhotos *int `json:"total_photos"` 40 | Private *bool `json:"private"` 41 | ShareKey *string `json:"share_key"` 42 | CoverPhoto *Photo `json:"cover_photo"` 43 | Photographer *User `json:"user"` 44 | Links *struct { 45 | Self *URL `json:"self"` 46 | HTML *URL `json:"html"` 47 | Photos *URL `json:"photos"` 48 | Related *URL `json:"related"` 49 | } `json:"links"` 50 | } 51 | 52 | func (c *Collection) String() string { 53 | var buffer bytes.Buffer 54 | buffer.WriteString("Collection: ") 55 | if c.Title != nil { 56 | buffer.WriteString(*c.Title) 57 | } 58 | buffer.WriteString("[ID:" + strconv.Itoa(*c.ID) + "]") 59 | return buffer.String() 60 | } 61 | -------------------------------------------------------------------------------- /unsplash/get_helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import "encoding/json" 27 | 28 | // getPhotos can be used to query any endpoint which returns an array of Photos 29 | func (s *service) getPhotos(opt *ListOpt, endpoint string) (*[]Photo, *Response, error) { 30 | if nil == opt { 31 | opt = defaultListOpt 32 | } 33 | if !opt.Valid() { 34 | return nil, nil, &InvalidListOptError{ErrString: "opt provided is not valid."} 35 | } 36 | req, err := newRequest(GET, endpoint, opt, nil) 37 | if err != nil { 38 | return nil, nil, err 39 | } 40 | resp, err := s.client.do(req) 41 | if err != nil { 42 | return nil, nil, err 43 | } 44 | photos := make([]Photo, 0) 45 | err = json.Unmarshal(*resp.body, &photos) 46 | if err != nil { 47 | return nil, nil, err 48 | } 49 | return &photos, resp, nil 50 | } 51 | 52 | // getCollections can be used to query any endpoint which 53 | //returns an array of Collections 54 | func (s *service) getCollections(opt *ListOpt, endpoint string) (*[]Collection, *Response, error) { 55 | if nil == opt { 56 | opt = defaultListOpt 57 | } 58 | if !opt.Valid() { 59 | return nil, nil, &InvalidListOptError{ErrString: "opt provided is not valid."} 60 | } 61 | req, err := newRequest(GET, endpoint, opt, nil) 62 | if err != nil { 63 | return nil, nil, err 64 | } 65 | resp, err := s.client.do(req) 66 | if err != nil { 67 | return nil, nil, err 68 | } 69 | collections := make([]Collection, 0) 70 | err = json.Unmarshal(*resp.body, &collections) 71 | if err != nil { 72 | return nil, nil, err 73 | } 74 | return &collections, resp, nil 75 | } 76 | -------------------------------------------------------------------------------- /unsplash/list_options.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "bytes" 28 | "strconv" 29 | ) 30 | 31 | //These constants should be used for OrderBy searches/results. 32 | const ( 33 | Latest = "latest" 34 | Oldest = "oldest" 35 | Popular = "popular" 36 | ) 37 | 38 | var orders = []string{"latest", "oldest", "popular"} 39 | 40 | // ListOpt should be used for pagination over results 41 | type ListOpt struct { 42 | Page int `url:"page"` 43 | PerPage int `url:"per_page"` 44 | OrderBy string `url:"order_by"` 45 | } 46 | 47 | var defaultListOpt = &ListOpt{ 48 | Page: 1, 49 | PerPage: 10, 50 | OrderBy: Popular, 51 | } 52 | 53 | // Valid validates the values in a ListOpt 54 | func (opt *ListOpt) Valid() bool { 55 | if opt.Page < 0 { 56 | return false 57 | } 58 | if opt.Page == 0 { 59 | opt.Page = 1 60 | } 61 | if opt.PerPage < 0 { 62 | return false 63 | } 64 | if opt.PerPage == 0 { 65 | opt.PerPage = 10 66 | } 67 | if opt.OrderBy == "" { 68 | opt.OrderBy = Latest 69 | } 70 | for _, val := range orders { 71 | if val == opt.OrderBy { 72 | return true 73 | } 74 | } 75 | return false 76 | } 77 | 78 | func (opt *ListOpt) String() string { 79 | var buf bytes.Buffer 80 | if !opt.Valid() { 81 | buf.WriteString("ListOpt is invalid") 82 | } 83 | buf.WriteString("ListOpt: Page[") 84 | buf.WriteString(strconv.Itoa(opt.Page)) 85 | buf.WriteString("], PerPage[") 86 | buf.WriteString(strconv.Itoa(opt.PerPage)) 87 | buf.WriteString("], OrderBy[") 88 | buf.WriteString(opt.OrderBy) 89 | buf.WriteString("]") 90 | return buf.String() 91 | } 92 | -------------------------------------------------------------------------------- /unsplash/errors_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "io/ioutil" 28 | "log" 29 | "testing" 30 | 31 | "github.com/stretchr/testify/assert" 32 | ) 33 | 34 | func TestErrors(T *testing.T) { 35 | assert := assert.New(T) 36 | log.SetOutput(ioutil.Discard) 37 | var err error 38 | 39 | err = &IllegalArgumentError{ErrString: "I'm Joker."} 40 | iae2, ok := err.(*IllegalArgumentError) 41 | assert.NotNil(iae2) 42 | log.Println(iae2) 43 | assert.Equal(true, ok) 44 | 45 | err = &AuthorizationError{ErrString: "I'm Batman."} 46 | ae, ok := err.(*AuthorizationError) 47 | assert.NotNil(ae) 48 | log.Println(ae) 49 | assert.Equal(true, ok) 50 | 51 | err = &JSONUnmarshallingError{ErrString: "I'm Batman."} 52 | jue, ok := err.(*JSONUnmarshallingError) 53 | assert.NotNil(jue) 54 | log.Println(jue) 55 | assert.Equal(true, ok) 56 | 57 | err = &NotFoundError{ErrString: "I'll find you and I'll kill you."} 58 | nfe, ok := err.(*NotFoundError) 59 | assert.NotNil(nfe) 60 | log.Println(nfe) 61 | assert.Equal(true, ok) 62 | 63 | err = &InvalidPhotoOptError{ErrString: "."} 64 | ipo, ok := err.(*InvalidPhotoOptError) 65 | assert.NotNil(ipo) 66 | log.Println(ipo) 67 | assert.Equal(true, ok) 68 | 69 | err = &InvalidListOptError{ErrString: "."} 70 | ilo, ok := err.(*InvalidListOptError) 71 | assert.NotNil(ilo) 72 | log.Println(ilo) 73 | assert.Equal(true, ok) 74 | 75 | err = &RateLimitError{ErrString: "."} 76 | rle, ok := err.(*RateLimitError) 77 | assert.NotNil(rle) 78 | log.Println(rle) 79 | assert.Equal(true, ok) 80 | 81 | err = &InvalidStatsOptError{ErrString: "."} 82 | iso, ok := err.(*InvalidStatsOptError) 83 | assert.NotNil(iso) 84 | log.Println(iso) 85 | assert.Equal(true, ok) 86 | } 87 | -------------------------------------------------------------------------------- /unsplash/URL_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "encoding/json" 28 | "io/ioutil" 29 | "log" 30 | "testing" 31 | 32 | "github.com/stretchr/testify/assert" 33 | ) 34 | 35 | var ( 36 | correctData = []string{ 37 | "{\"URL\":\"https://unsplash.com/@hbagdi/likes\"}", 38 | "{\"URL\":\"https://unsplash.com/documentation#get-the-users-profile\"}", 39 | "{\"URL\":\"https://en.wikipedia.org/wiki/42_(number)\"}", 40 | } 41 | badJSON = "{\"Anything that can go wrong, will go wrong.\"}" 42 | badURL = "{\"URL\":\"httpps://en.shttps:////\"}" 43 | 44 | trimmedURL = "https://unsplash.com/@hbagdi/likes" 45 | whitespaceURL = "{\"URL\":\"" + trimmedURL + " \"}" 46 | ) 47 | 48 | type URLWrapper struct { 49 | OURL *URL `json:"URL,omitempty"` 50 | } 51 | 52 | func TestURL(T *testing.T) { 53 | assert := assert.New(T) 54 | log.SetOutput(ioutil.Discard) 55 | 56 | for _, value := range correctData { 57 | bytes := []byte(value) 58 | var url URLWrapper 59 | err := json.Unmarshal(bytes, &url) 60 | assert.Nil(err) 61 | log.Println("Unmarshalled string: ", url.OURL.String()) 62 | marshalBytes, err := json.Marshal(url) 63 | assert.Nil(err) 64 | log.Println("Marshalled string: ", string(marshalBytes)) 65 | assert.Equal(string(marshalBytes), value) 66 | } 67 | 68 | log.SetOutput(ioutil.Discard) 69 | var url URLWrapper 70 | bytes := []byte(badJSON) 71 | err := json.Unmarshal(bytes, &url) 72 | assert.NotNil(err) 73 | log.Println(err) 74 | assert.Nil(url.OURL) 75 | 76 | var url2 URL 77 | err = url2.UnmarshalJSON([]byte(badURL)) 78 | assert.NotNil(err) 79 | 80 | var url3 URL 81 | err = url3.UnmarshalJSON([]byte(badJSON)) 82 | assert.NotNil(err) 83 | 84 | bytes = []byte(whitespaceURL) 85 | var url4 URLWrapper 86 | err = json.Unmarshal(bytes, &url4) 87 | assert.Nil(err) 88 | assert.Equal(trimmedURL, url4.OURL.String()) 89 | } 90 | -------------------------------------------------------------------------------- /unsplash/endpoints.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | type method string 27 | 28 | const ( 29 | // GET is HTTP GET request 30 | GET method = "GET" 31 | // POST is HTTP POST request 32 | POST method = "POST" 33 | // PUT is HTTP PUT request 34 | PUT method = "PUT" 35 | // DELETE is HTTP DELETE request 36 | DELETE method = "DELETE" 37 | ) 38 | 39 | const ( 40 | currentUserEndpoint = "me" 41 | globalStatsEndpoint = "stats/total" 42 | monthStatsEndpoint = "stats/month" 43 | usersEndpoint = "users" 44 | photosEndpoint = "photos" 45 | collectionsEndpoint = "collections" 46 | searchEndpoint = "search" 47 | searchUserEndpoint = searchEndpoint + "/" + usersEndpoint 48 | searchPhotosEndpoint = searchEndpoint + "/" + photosEndpoint 49 | searchCollectionsEndpoint = searchEndpoint + "/" + collectionsEndpoint 50 | ) 51 | 52 | var apiBaseURL = "https://api.unsplash.com/" 53 | 54 | type endpoint int 55 | 56 | const ( 57 | base endpoint = iota 58 | currentUser 59 | globalStats 60 | monthStats 61 | users 62 | photos 63 | collections 64 | searchUsers 65 | searchPhotos 66 | searchCollections 67 | ) 68 | 69 | var mapURL map[endpoint]string 70 | 71 | func init() { 72 | mapURL = make(map[endpoint]string) 73 | mapURL[base] = apiBaseURL 74 | mapURL[currentUser] = currentUserEndpoint 75 | mapURL[globalStats] = globalStatsEndpoint 76 | mapURL[monthStats] = monthStatsEndpoint 77 | mapURL[users] = usersEndpoint 78 | mapURL[photos] = photosEndpoint 79 | mapURL[collections] = collectionsEndpoint 80 | mapURL[searchUsers] = searchUserEndpoint 81 | mapURL[searchPhotos] = searchPhotosEndpoint 82 | mapURL[searchCollections] = searchCollectionsEndpoint 83 | } 84 | 85 | func getEndpoint(e endpoint) string { 86 | return mapURL[e] 87 | } 88 | 89 | func SetupBaseUrl(url string) { 90 | apiBaseURL = url 91 | mapURL[base] = apiBaseURL 92 | } 93 | -------------------------------------------------------------------------------- /unsplash/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | //The following are implementing error interface 27 | 28 | // IllegalArgumentError occurs when the argument to a function are 29 | // messed up 30 | type IllegalArgumentError struct { 31 | ErrString string 32 | } 33 | 34 | func (e IllegalArgumentError) Error() string { 35 | return e.ErrString 36 | } 37 | 38 | // JSONUnmarshallingError occurs due to a unmarshalling error 39 | type JSONUnmarshallingError struct { 40 | ErrString string 41 | } 42 | 43 | func (e JSONUnmarshallingError) Error() string { 44 | return e.ErrString 45 | } 46 | 47 | // AuthorizationError occurs for an Unauthorized request 48 | type AuthorizationError struct { 49 | ErrString string 50 | } 51 | 52 | func (e AuthorizationError) Error() string { 53 | return e.ErrString 54 | } 55 | 56 | // NotFoundError occurs when the resource queried returns a 404. 57 | type NotFoundError struct { 58 | ErrString string 59 | } 60 | 61 | func (e NotFoundError) Error() string { 62 | return e.ErrString 63 | } 64 | 65 | // InvalidPhotoOptError occurs when PhotoOpt.Valid() fails. 66 | type InvalidPhotoOptError struct { 67 | ErrString string 68 | } 69 | 70 | func (e InvalidPhotoOptError) Error() string { 71 | return e.ErrString 72 | } 73 | 74 | // InvalidListOptError occurs when ListOpt.Valid() fails. 75 | type InvalidListOptError struct { 76 | ErrString string 77 | } 78 | 79 | func (e InvalidListOptError) Error() string { 80 | return e.ErrString 81 | } 82 | 83 | // InvalidStatsOptError occurs when StatsOpt.Valid() fails. 84 | type InvalidStatsOptError struct { 85 | ErrString string 86 | } 87 | 88 | func (e InvalidStatsOptError) Error() string { 89 | return e.ErrString 90 | } 91 | 92 | // RateLimitError occurs when rate limit is reached for the API key. 93 | type RateLimitError struct { 94 | ErrString string 95 | } 96 | 97 | func (e RateLimitError) Error() string { 98 | return e.ErrString 99 | } 100 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hardikbagdi@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /unsplash/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | /* 25 | Package unsplash provides a RESTful go binding for https//:unsplash.com API. 26 | 27 | 28 | Usage 29 | 30 | Use the following import path: 31 | import "github.com/hbagdi/go-unsplash/unsplash" 32 | 33 | Authentication 34 | 35 | Authentication is not handled by directly by go-unsplash. 36 | Instead, pass an http.Client 37 | that can handle authentication for you. 38 | You can use libraries such as https://godoc.org/golang.org/x/oauth2. 39 | Please note that all calls will include the OAuth token and hence, http.Client 40 | should not be shared between users. 41 | Note that if you're just using actions that require the public permission scope, 42 | only the AppID is required. 43 | 44 | 45 | Creating an instance 46 | 47 | An instance of unsplash can be created using New(). 48 | The http.Client supplied will be used to make requests to the API. 49 | 50 | ts := oauth2.StaticTokenSource( 51 | &oauth2.Token{AccessToken: "Your-access-token"}, 52 | ) 53 | client := oauth2.NewClient(oauth2.NoContext, ts) 54 | //use the http.Client to instantiate unsplash 55 | unsplash := unsplash.New(client) 56 | // requests can be now made to the API 57 | randomPhoto, _ , err := unsplash.RandomPhoto(nil) 58 | 59 | Error handling 60 | 61 | All API calls return an error as second or third return object. 62 | All successful calls will return nil in place of this return. 63 | Further, go-unsplash has errors defined as types for better error handling. 64 | 65 | randomPhoto, _ , err := unsplash.RandomPhoto(nil) 66 | if err != nil { 67 | //handle error 68 | } 69 | 70 | Pagination 71 | 72 | Pagination is supported by supplying a page 73 | number in the ListOpt. 74 | The NextPage field in Response can be used to get the next page number. 75 | 76 | var allPhotos []*unsplash.Photo 77 | searchOpt := &unsplash.SearchOpt{Query: "Batman"} 78 | for { 79 | photos, resp, err := unsplash.Search.Photos(searchOpt) 80 | 81 | if err != nil { 82 | return 83 | } 84 | 85 | allPhotos = append(allPhotos, photos) 86 | 87 | if !resp.HasNextPage { 88 | break 89 | } 90 | searchOpt.Page = resp.NextPage 91 | } 92 | 93 | */ 94 | package unsplash 95 | -------------------------------------------------------------------------------- /unsplash/search_service.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import "encoding/json" 27 | 28 | // SearchService interacts with /search endpoint 29 | type SearchService service 30 | 31 | // SearchOpt should be used to query /search endpoint 32 | type SearchOpt struct { 33 | Page int `url:"page"` 34 | PerPage int `url:"per_page"` 35 | Query string `url:"query"` 36 | } 37 | 38 | // Valid validates a SearchOpt 39 | func (opt *SearchOpt) Valid() bool { 40 | if opt.Query == "" { 41 | return false 42 | } 43 | // default params 44 | if opt.Page == 0 { 45 | opt.Page = 1 46 | } 47 | if opt.PerPage == 0 { 48 | opt.PerPage = 10 49 | } 50 | return true 51 | } 52 | 53 | // Users can be used to query any endpoint which returns an array of users. 54 | func (ss *SearchService) Users(opt *SearchOpt) (*UserSearchResult, *Response, error) { 55 | if nil == opt { 56 | return nil, nil, &IllegalArgumentError{ErrString: "SearchOpt cannot be nil"} 57 | } 58 | if !opt.Valid() { 59 | return nil, nil, &InvalidListOptError{ErrString: "Search query cannot be empty."} 60 | } 61 | req, err := newRequest(GET, getEndpoint(searchUsers), opt, nil) 62 | if err != nil { 63 | return nil, nil, err 64 | } 65 | resp, err := ss.client.do(req) 66 | if err != nil { 67 | return nil, nil, err 68 | } 69 | var users UserSearchResult 70 | err = json.Unmarshal(*resp.body, &users) 71 | if err != nil { 72 | return nil, nil, err 73 | } 74 | return &users, resp, nil 75 | } 76 | 77 | // Photos queries the search endpoint to search for photos. 78 | func (ss *SearchService) Photos(opt *SearchOpt) (*PhotoSearchResult, *Response, error) { 79 | if nil == opt { 80 | return nil, nil, &IllegalArgumentError{ErrString: "SearchOpt cannot be nil"} 81 | } 82 | if !opt.Valid() { 83 | return nil, nil, &InvalidListOptError{ErrString: "Search query cannot be empty."} 84 | } 85 | req, err := newRequest(GET, getEndpoint(searchPhotos), opt, nil) 86 | if err != nil { 87 | return nil, nil, err 88 | } 89 | resp, err := ss.client.do(req) 90 | if err != nil { 91 | return nil, nil, err 92 | } 93 | var photos PhotoSearchResult 94 | err = json.Unmarshal(*resp.body, &photos) 95 | if err != nil { 96 | return nil, nil, err 97 | } 98 | return &photos, resp, nil 99 | } 100 | 101 | // Collections queries the search endpoint to search for collections. 102 | func (ss *SearchService) Collections(opt *SearchOpt) (*CollectionSearchResult, *Response, error) { 103 | if nil == opt { 104 | return nil, nil, &IllegalArgumentError{ErrString: "SearchOpt cannot be nil"} 105 | } 106 | if !opt.Valid() { 107 | return nil, nil, &InvalidListOptError{ErrString: "Search query cannot be empty."} 108 | } 109 | req, err := newRequest(GET, getEndpoint(searchCollections), opt, nil) 110 | if err != nil { 111 | return nil, nil, err 112 | } 113 | resp, err := ss.client.do(req) 114 | if err != nil { 115 | return nil, nil, err 116 | } 117 | var collections CollectionSearchResult 118 | err = json.Unmarshal(*resp.body, &collections) 119 | if err != nil { 120 | return nil, nil, err 121 | } 122 | return &collections, resp, nil 123 | } 124 | -------------------------------------------------------------------------------- /unsplash/search_service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "io/ioutil" 28 | "log" 29 | "testing" 30 | 31 | "github.com/jarcoal/httpmock" 32 | "github.com/stretchr/testify/assert" 33 | ) 34 | 35 | func TestSearchPhotos(T *testing.T) { 36 | log.SetOutput(ioutil.Discard) 37 | assert := assert.New(T) 38 | assert.Nil(nil) 39 | log.SetOutput(ioutil.Discard) 40 | unsplash := setup() 41 | var opt SearchOpt 42 | photos, resp, err := unsplash.Search.Photos(&opt) 43 | assert.NotNil(err) 44 | assert.Nil(resp) 45 | assert.Nil(photos) 46 | opt.Query = "Nature" 47 | photos, _, err = unsplash.Search.Photos(&opt) 48 | log.Println(len(*photos.Results)) 49 | assert.NotNil(photos) 50 | assert.Nil(err) 51 | log.Println(photos) 52 | 53 | photos, resp, err = unsplash.Search.Photos(nil) 54 | assert.NotNil(err) 55 | assert.Nil(resp) 56 | assert.Nil(photos) 57 | } 58 | 59 | func TestSearchUsers(T *testing.T) { 60 | log.SetOutput(ioutil.Discard) 61 | assert := assert.New(T) 62 | assert.Nil(nil) 63 | log.SetOutput(ioutil.Discard) 64 | unsplash := setup() 65 | var opt SearchOpt 66 | users, resp, err := unsplash.Search.Users(&opt) 67 | assert.NotNil(err) 68 | assert.Nil(resp) 69 | assert.Nil(users) 70 | opt.Query = "Nature" 71 | users, _, err = unsplash.Search.Users(&opt) 72 | log.Println(len(*users.Results)) 73 | assert.NotNil(users) 74 | assert.Nil(err) 75 | log.Println(users) 76 | 77 | users, resp, err = unsplash.Search.Users(nil) 78 | assert.NotNil(err) 79 | assert.Nil(resp) 80 | assert.Nil(users) 81 | } 82 | 83 | func TestSearchCollections(T *testing.T) { 84 | T.Skip() 85 | log.SetOutput(ioutil.Discard) 86 | assert := assert.New(T) 87 | assert.Nil(nil) 88 | log.SetOutput(ioutil.Discard) 89 | unsplash := setup() 90 | var opt SearchOpt 91 | collections, resp, err := unsplash.Search.Collections(&opt) 92 | assert.NotNil(err) 93 | assert.Nil(resp) 94 | assert.Nil(collections) 95 | opt.Query = "Nature" 96 | collections, _, err = unsplash.Search.Collections(&opt) 97 | assert.NotNil(collections) 98 | assert.Nil(err) 99 | log.Println(collections) 100 | log.Println(len(*collections.Results)) 101 | 102 | collections, resp, err = unsplash.Search.Collections(nil) 103 | assert.NotNil(err) 104 | assert.Nil(resp) 105 | assert.Nil(collections) 106 | } 107 | 108 | func rogueSearchServiceTest(T *testing.T, responder httpmock.Responder) { 109 | httpmock.Activate() 110 | defer httpmock.DeactivateAndReset() 111 | log.SetOutput(ioutil.Discard) 112 | qs := "?page=1&per_page=10&query=InnerPeace" 113 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(searchCollections)+qs, 114 | responder) 115 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(searchUsers)+qs, 116 | responder) 117 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(searchPhotos)+qs, 118 | responder) 119 | 120 | unsplash := setup() 121 | assert := assert.New(T) 122 | var opt SearchOpt 123 | opt.Query = "InnerPeace" 124 | photos, resp, err := unsplash.Search.Photos(&opt) 125 | assert.Nil(photos) 126 | assert.Nil(resp) 127 | assert.NotNil(err) 128 | log.Println(err) 129 | 130 | collections, resp, err := unsplash.Search.Collections(&opt) 131 | assert.Nil(collections) 132 | assert.Nil(resp) 133 | assert.NotNil(err) 134 | log.Println(err) 135 | users, resp, err := unsplash.Search.Users(&opt) 136 | assert.Nil(users) 137 | assert.Nil(resp) 138 | assert.NotNil(err) 139 | log.Println(err) 140 | } 141 | 142 | func TestSearchRogueStuff(T *testing.T) { 143 | rogueSearchServiceTest(T, httpmock.NewStringResponder(200, `Bad ass Bug flow`)) 144 | rogueSearchServiceTest(T, nil) 145 | } 146 | -------------------------------------------------------------------------------- /unsplash/response.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "bytes" 28 | "errors" 29 | "io/ioutil" 30 | "net/http" 31 | "net/url" 32 | "strconv" 33 | "strings" 34 | ) 35 | 36 | // Response has pagination information whenever applicable 37 | type Response struct { 38 | httpResponse *http.Response 39 | HasNextPage bool 40 | body *[]byte 41 | FirstPage, LastPage, NextPage, PrevPage int 42 | err error 43 | RateLimit int 44 | RateLimitRemaining int 45 | } 46 | 47 | func (r *Response) checkForErrors() error { 48 | switch r.httpResponse.StatusCode { 49 | case 200, 201, 202, 204, 205: 50 | return nil 51 | case 401: 52 | return &AuthorizationError{ErrString: errStringHelper(r.httpResponse.StatusCode, "Unauthorized request", r.body)} 53 | case 403: 54 | if r.RateLimitRemaining == 0 { 55 | return &RateLimitError{ErrString: errStringHelper(r.httpResponse.StatusCode, "Rate limit exhausted", r.body)} 56 | } 57 | 58 | return &AuthorizationError{ErrString: errStringHelper(r.httpResponse.StatusCode, "Access forbidden request", r.body)} 59 | 60 | case 404: 61 | return &NotFoundError{ErrString: errStringHelper(r.httpResponse.StatusCode, "The cat got tired of the Laser", r.body)} 62 | default: 63 | return errors.New(errStringHelper(r.httpResponse.StatusCode, "API returned an error", r.body)) 64 | 65 | } 66 | } 67 | func errStringHelper(statusCode int, msg string, errBody *[]byte) string { 68 | var buf bytes.Buffer 69 | //XXX Writes can fail? 70 | buf.WriteString(strconv.Itoa(statusCode)) 71 | buf.WriteString(": ") 72 | buf.WriteString(msg) 73 | buf.WriteString(", Body: ") 74 | buf.Write(*errBody) 75 | return buf.String() 76 | } 77 | 78 | func newResponse(r *http.Response) (*Response, error) { 79 | if nil == r { 80 | return nil, 81 | &IllegalArgumentError{ErrString: "*http.Response cannot be null"} 82 | } 83 | resp := new(Response) 84 | resp.httpResponse = r 85 | //populate first 86 | resp.populatePagingInfo() 87 | resp.populateRateLimits() 88 | //read the response 89 | buf, err := ioutil.ReadAll(resp.httpResponse.Body) 90 | if err != nil { 91 | return nil, err 92 | } 93 | resp.body = &buf 94 | //now check for errors 95 | err = resp.checkForErrors() 96 | if err != nil { 97 | return nil, err 98 | } 99 | return resp, nil 100 | } 101 | 102 | func (r *Response) populateRateLimits() { 103 | //fails silently 104 | maxLimit, ok := r.httpResponse.Header["X-Ratelimit-Limit"] 105 | if ok && len(maxLimit) == 1 { 106 | r.RateLimit, _ = strconv.Atoi(maxLimit[0]) 107 | } 108 | rateRemaining, ok := r.httpResponse.Header["X-Ratelimit-Remaining"] 109 | if ok && len(rateRemaining) == 1 { 110 | r.RateLimitRemaining, _ = strconv.Atoi(rateRemaining[0]) 111 | } 112 | } 113 | 114 | func (r *Response) populatePagingInfo() { 115 | //fails silently 116 | rawLinks, ok := r.httpResponse.Header["Link"] 117 | if !ok || 0 == len(rawLinks) { 118 | return 119 | } 120 | 121 | links := strings.Split(rawLinks[0], ",") 122 | 123 | for _, link := range links { 124 | parts := strings.Split(link, ";") 125 | if !strings.Contains(parts[0], "page") && !strings.Contains(parts[1], "rel=") { 126 | continue 127 | } 128 | href := parts[0] 129 | //strip out '<' and '>' 130 | href = href[strings.Index(href, "<")+1 : strings.Index(href, ">")] 131 | url, err := url.Parse(href) 132 | if err != nil { 133 | continue 134 | } 135 | pageString := url.Query().Get("page") 136 | pageNumber, err := strconv.Atoi(string(pageString)) 137 | if err != nil { 138 | continue 139 | } 140 | 141 | switch strings.TrimSpace(parts[1]) { 142 | case `rel="first"`: 143 | r.FirstPage = pageNumber 144 | case `rel="last"`: 145 | r.LastPage = pageNumber 146 | case `rel="next"`: 147 | r.NextPage = pageNumber 148 | r.HasNextPage = true 149 | case `rel="prev"`: 150 | r.PrevPage = pageNumber 151 | default: 152 | continue 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /unsplash/users_service.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "encoding/json" 28 | "fmt" 29 | ) 30 | 31 | // ProfileImageOpt denotes properties of any Image 32 | type ProfileImageOpt struct { 33 | Height int `json:"h,omitempty" url:"h"` 34 | Width int `json:"w,omitempty" url:"w"` 35 | } 36 | 37 | // UsersService interacts with /users endpoint 38 | type UsersService service 39 | 40 | // User returns a User with username and optional profile image size ImageOpt 41 | func (us *UsersService) User(username string, imageOpt *ProfileImageOpt) (*User, error) { 42 | if "" == username { 43 | return nil, &IllegalArgumentError{ErrString: "Username cannot be null"} 44 | } 45 | endpoint := fmt.Sprintf("%v/%v", getEndpoint(users), username) 46 | req, err := newRequest(GET, endpoint, imageOpt, nil) 47 | if err != nil { 48 | return nil, err 49 | } 50 | resp, err := us.client.do(req) 51 | if err != nil { 52 | return nil, err 53 | } 54 | var user User 55 | err = json.Unmarshal(*resp.body, &user) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return &user, nil 60 | } 61 | 62 | type urlWrapper struct { 63 | URL *URL `json:"url"` 64 | } 65 | 66 | // Portfolio returns a User with username and optional profile image size ImageOpt 67 | func (us *UsersService) Portfolio(username string) (*URL, error) { 68 | if "" == username { 69 | return nil, &IllegalArgumentError{ErrString: "Username cannot be null"} 70 | } 71 | endpoint := fmt.Sprintf("%v/%v/portfolio", getEndpoint(users), username) 72 | req, err := newRequest(GET, endpoint, nil, nil) 73 | if err != nil { 74 | return nil, err 75 | } 76 | resp, err := us.client.do(req) 77 | if err != nil { 78 | return nil, err 79 | } 80 | var portfolio urlWrapper 81 | err = json.Unmarshal(*resp.body, &portfolio) 82 | if err != nil { 83 | return nil, err 84 | } 85 | return portfolio.URL, nil 86 | } 87 | 88 | // Photos return an array of photos uploaded by the user. 89 | func (us *UsersService) Photos(username string, opt *ListOpt) (*[]Photo, *Response, error) { 90 | if "" == username { 91 | return nil, nil, &IllegalArgumentError{ErrString: "Username cannot be null"} 92 | } 93 | s := (service)(*us) 94 | endpoint := fmt.Sprintf("%v/%v/%v", getEndpoint(users), username, getEndpoint(photos)) 95 | return s.getPhotos(opt, endpoint) 96 | } 97 | 98 | // LikedPhotos return an array of liked photos 99 | func (us *UsersService) LikedPhotos(username string, opt *ListOpt) (*[]Photo, *Response, error) { 100 | if "" == username { 101 | return nil, nil, &IllegalArgumentError{ErrString: "Username cannot be null"} 102 | } 103 | s := (service)(*us) 104 | endpoint := fmt.Sprintf("%v/%v/%v", getEndpoint(users), username, "likes") 105 | return s.getPhotos(opt, endpoint) 106 | } 107 | 108 | // Collections return an array of user's collections. 109 | func (us *UsersService) Collections(username string, opt *ListOpt) (*[]Collection, *Response, error) { 110 | if "" == username { 111 | return nil, nil, &IllegalArgumentError{ErrString: "Username cannot be null"} 112 | } 113 | s := (service)(*us) 114 | endpoint := fmt.Sprintf("%v/%v/%v", getEndpoint(users), username, getEndpoint(collections)) 115 | return s.getCollections(opt, endpoint) 116 | } 117 | 118 | // Statistics return a stats about a photo with id. 119 | func (us *UsersService) Statistics(username string, opt *StatsOpt) (*UserStatistics, *Response, error) { 120 | if "" == username { 121 | return nil, nil, &IllegalArgumentError{ErrString: "Photo ID cannot be null"} 122 | } 123 | if opt == nil { 124 | opt = defaultStatsOpt 125 | } 126 | if !opt.Valid() { 127 | return nil, nil, &InvalidStatsOptError{ErrString: "opt provided is not valid."} 128 | } 129 | endpoint := fmt.Sprintf("%v/%v/statistics", getEndpoint(users), username) 130 | req, err := newRequest(GET, endpoint, opt, nil) 131 | if err != nil { 132 | return nil, nil, err 133 | } 134 | resp, err := us.client.do(req) 135 | if err != nil { 136 | return nil, nil, err 137 | } 138 | var stats UserStatistics 139 | err = json.Unmarshal(*resp.body, &stats) 140 | if err != nil { 141 | return nil, nil, err 142 | } 143 | return &stats, resp, nil 144 | } 145 | -------------------------------------------------------------------------------- /unsplash/photo.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "bytes" 28 | "time" 29 | ) 30 | 31 | //ExifData for an image 32 | type ExifData struct { 33 | Make *string `json:"make"` 34 | Model *string `json:"model"` 35 | ExposureTime *string `json:"exposure_time"` 36 | Aperture *string `json:"aperture"` 37 | FocalLength *string `json:"focal_length"` 38 | Iso *int `json:"iso"` 39 | } 40 | 41 | // Tag lists can be applied to any photo 42 | type Tag struct { 43 | Type *string `json:"type"` 44 | Title *string `json:"title"` 45 | } 46 | 47 | // Photo represents a photo on unsplash.com 48 | type Photo struct { 49 | ID *string `json:"id"` 50 | CreatedAt *time.Time `json:"created_at"` 51 | UpdatedAt *time.Time `json:"updated_at"` 52 | Width *int `json:"width"` 53 | Height *int `json:"height"` 54 | Color *string `json:"color"` 55 | Description *string `json:"description"` 56 | AltDescription *string `json:"alt_description"` 57 | Views *int `json:"views"` 58 | Downloads *int `json:"downloads"` 59 | Likes *int `json:"likes"` 60 | LikedByUser *bool `json:"liked_by_user"` 61 | Exif *ExifData `json:"exif"` 62 | Photographer *User `json:"user"` 63 | Location *struct { 64 | Title *string `json:"title"` 65 | Name *string `json:"name"` 66 | City *string `json:"city"` 67 | Country *string `json:"country"` 68 | Position *struct { 69 | Latitude *float64 `json:"latitude"` 70 | Longitude *float64 `json:"longitude"` 71 | } `json:"position"` 72 | } `json:"location"` 73 | Tags *[]Tag `json:"tags"` 74 | CurrentUserCollections *[]Collection `json:"current_user_collections"` 75 | Urls *struct { 76 | Raw *URL `json:"raw"` 77 | Full *URL `json:"full"` 78 | Regular *URL `json:"regular"` 79 | Small *URL `json:"small"` 80 | Thumb *URL `json:"thumb"` 81 | Custom *URL `json:"custom"` 82 | } `json:"urls"` 83 | Links *struct { 84 | Self *URL `json:"self"` 85 | HTML *URL `json:"html"` 86 | Download *URL `json:"download"` 87 | DownloadLocation *URL `json:"download_location"` 88 | } `json:"links"` 89 | } 90 | 91 | func (p *Photo) String() string { 92 | var buf bytes.Buffer 93 | if p.ID == nil { 94 | return "Photo is not valid" 95 | } 96 | buf.WriteString("Photo: ID[") 97 | buf.WriteString(*p.ID) 98 | buf.WriteString("]") 99 | return buf.String() 100 | } 101 | 102 | //PhotoStats shows various stats of the photo returned by /photos/:id/stats endpoint 103 | type PhotoStats struct { 104 | Downloads int `json:"downloads"` 105 | Likes int `json:"likes"` 106 | Views int `json:"views"` 107 | Links struct { 108 | Self string `json:"self"` 109 | HTML string `json:"html"` 110 | Download string `json:"download"` 111 | DownloadLocation string `json:"download_location"` 112 | } `json:"links"` 113 | } 114 | 115 | // PhotoStatistics represents statistics like downloads, views and likes of an unsplash photo 116 | type PhotoStatistics struct { 117 | ID string `json:"id"` 118 | Downloads struct { 119 | Total int `json:"total"` 120 | Historical struct { 121 | Change int `json:"change"` 122 | Resolution string `json:"resolution"` 123 | Quantity int `json:"quantity"` 124 | Values []struct { 125 | Date string `json:"date"` 126 | Value int `json:"value"` 127 | } `json:"values"` 128 | } `json:"historical"` 129 | } `json:"downloads"` 130 | Views struct { 131 | Total int `json:"total"` 132 | Historical struct { 133 | Change int `json:"change"` 134 | Resolution string `json:"resolution"` 135 | Quantity int `json:"quantity"` 136 | Values []struct { 137 | Date string `json:"date"` 138 | Value int `json:"value"` 139 | } `json:"values"` 140 | } `json:"historical"` 141 | } `json:"views"` 142 | Likes struct { 143 | Total int `json:"total"` 144 | Historical struct { 145 | Change int `json:"change"` 146 | Resolution string `json:"resolution"` 147 | Quantity int `json:"quantity"` 148 | Values []struct { 149 | Date string `json:"date"` 150 | Value int `json:"value"` 151 | } `json:"values"` 152 | } `json:"historical"` 153 | } `json:"likes"` 154 | } 155 | -------------------------------------------------------------------------------- /unsplash/unsplash.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "encoding/json" 28 | "fmt" 29 | "io" 30 | "io/ioutil" 31 | "net/http" 32 | ) 33 | 34 | type service struct { 35 | client *Unsplash 36 | } 37 | 38 | // Unsplash wraps the entire Unsplash.com API 39 | type Unsplash struct { 40 | client_id string 41 | client *http.Client 42 | common service 43 | Users *UsersService 44 | Photos *PhotosService 45 | Collections *CollectionsService 46 | Search *SearchService 47 | } 48 | 49 | //New returns a new Unsplash struct 50 | func New(client *http.Client) *Unsplash { 51 | if client == nil { 52 | client = http.DefaultClient 53 | } 54 | unsplash := new(Unsplash) 55 | unsplash.client = client 56 | unsplash.common.client = unsplash 57 | unsplash.Users = (*UsersService)(&unsplash.common) 58 | unsplash.Photos = (*PhotosService)(&unsplash.common) 59 | unsplash.Collections = (*CollectionsService)(&unsplash.common) 60 | unsplash.Search = (*SearchService)(&unsplash.common) 61 | return unsplash 62 | } 63 | 64 | //New returns a new Unsplash struct using client_id for Authorization header 65 | //This will only enable API that do not require user level authorization, but 66 | //just application level. 67 | func NewWithClientID(client *http.Client, client_id string) *Unsplash { 68 | r := New(client) 69 | r.client_id = client_id 70 | return r 71 | } 72 | 73 | func (s *Unsplash) do(req *request) (*Response, error) { 74 | var err error 75 | //TODO should this be exported? 76 | if req == nil { 77 | return nil, 78 | &IllegalArgumentError{ErrString: "Request object cannot be nil"} 79 | } 80 | req.Request.Header.Set("Accept-Version", "v1") 81 | if s.client_id != "" { 82 | req.Request.Header.Set("Authorization", fmt.Sprintf("Client-ID %v", s.client_id)) 83 | } 84 | //Make the request 85 | client := s.client 86 | rawResp, err := client.Do(req.Request) 87 | if rawResp != nil { 88 | defer rawResp.Body.Close() 89 | } 90 | if err != nil { 91 | return nil, err 92 | } 93 | resp, err := newResponse(rawResp) 94 | if err != nil { 95 | return nil, err 96 | } 97 | _, err = io.Copy(ioutil.Discard, rawResp.Body) 98 | if err != nil { 99 | return nil, err 100 | } 101 | return resp, nil 102 | } 103 | 104 | // CurrentUser returns details about the authenticated user 105 | func (u *Unsplash) CurrentUser() (*User, *Response, error) { 106 | var err error 107 | req, err := newRequest(GET, getEndpoint(currentUser), nil, nil) 108 | if err != nil { 109 | return nil, nil, err 110 | } 111 | resp, err := u.do(req) 112 | if err != nil { 113 | return nil, nil, err 114 | } 115 | user := new(User) 116 | err = json.Unmarshal(*resp.body, &user) 117 | if err != nil { 118 | return nil, nil, 119 | &JSONUnmarshallingError{ErrString: err.Error()} 120 | } 121 | return user, resp, nil 122 | } 123 | 124 | // UpdateCurrentUser updates the current user's private data and returns an update User struct 125 | func (u *Unsplash) UpdateCurrentUser(updateInfo *UserUpdateInfo) (*User, *Response, error) { 126 | if updateInfo == nil { 127 | return nil, nil, &IllegalArgumentError{ErrString: "updateInfo cannot be null"} 128 | } 129 | endpoint := "me" 130 | req, err := newRequest(PUT, endpoint, updateInfo, nil) 131 | if err != nil { 132 | return nil, nil, err 133 | } 134 | resp, err := u.do(req) 135 | if err != nil { 136 | return nil, nil, err 137 | } 138 | user := new(User) 139 | err = json.Unmarshal(*resp.body, &user) 140 | if err != nil { 141 | return nil, nil, 142 | &JSONUnmarshallingError{ErrString: err.Error()} 143 | } 144 | return user, resp, nil 145 | } 146 | 147 | // Stats gives the total photos,download since the inception of unsplash.com 148 | // This method is DEPRECATED, USE TotalStats() 149 | func (u *Unsplash) Stats() (*GlobalStats, *Response, error) { 150 | var err error 151 | req, err := newRequest(GET, getEndpoint(globalStats), nil, nil) 152 | if err != nil { 153 | return nil, nil, err 154 | } 155 | resp, err := u.do(req) 156 | if err != nil { 157 | return nil, nil, err 158 | } 159 | globalStats := new(GlobalStats) 160 | err = json.Unmarshal(*resp.body, &globalStats) 161 | if err != nil { 162 | return nil, nil, 163 | &JSONUnmarshallingError{ErrString: err.Error()} 164 | } 165 | return globalStats, resp, nil 166 | } 167 | 168 | // TotalStats returns various stats related to unsplash.com since it's inception 169 | func (u *Unsplash) TotalStats() (*GlobalStats, *Response, error) { 170 | return u.Stats() 171 | } 172 | 173 | // MonthStats returns various stats related to unsplash.com for last 30 days 174 | func (u *Unsplash) MonthStats() (*MonthStats, *Response, error) { 175 | var err error 176 | req, err := newRequest(GET, getEndpoint(monthStats), nil, nil) 177 | if err != nil { 178 | return nil, nil, err 179 | } 180 | resp, err := u.do(req) 181 | if err != nil { 182 | return nil, nil, err 183 | } 184 | monthStats := new(MonthStats) 185 | err = json.Unmarshal(*resp.body, &monthStats) 186 | if err != nil { 187 | return nil, nil, 188 | &JSONUnmarshallingError{ErrString: err.Error()} 189 | } 190 | return monthStats, resp, nil 191 | } 192 | -------------------------------------------------------------------------------- /unsplash/user.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import "bytes" 27 | 28 | // ProfileImage contains URLs to profile image of a user 29 | type ProfileImage struct { 30 | Small *URL `json:"small"` 31 | Medium *URL `json:"medium"` 32 | Large *URL `json:"large"` 33 | Custom *URL `json:"custom"` 34 | } 35 | 36 | // UserLinks contains URLs to 37 | type UserLinks struct { 38 | Followers *URL `json:"followers"` 39 | Following *URL `json:"following"` 40 | Self *URL `json:"self"` 41 | HTML *URL `json:"html"` 42 | Photos *URL `json:"photos"` 43 | Likes *URL `json:"likes"` 44 | Portfolio *URL `json:"portfolio"` 45 | } 46 | 47 | // UserBadge contains information about badge for a user 48 | type UserBadge struct { 49 | Title *URL `json:"title,omitempty"` 50 | Primary *bool `json:"primary,omitempty"` 51 | Slug *string `json:"slug,omitempty"` 52 | Link *URL `json:"link,omitempty"` 53 | } 54 | 55 | // User represents a Unsplash.com user 56 | type User struct { 57 | UID *string `json:"uid"` 58 | ID *string `json:"id"` 59 | Username *string `json:"username"` 60 | Name *string `json:"name"` 61 | FirstName *string `json:"first_name"` 62 | CompletedOnboarding *bool `json:"completed_onboarding"` 63 | LastName *string `json:"last_name,omitempty"` 64 | PortfolioURL *URL `json:"portfolio_url"` 65 | Bio *string `json:"bio"` 66 | Location *string `json:"location"` 67 | TotalLikes *int `json:"total_likes"` 68 | TotalPhotos *int `json:"total_photos"` 69 | TotalCollections *int `json:"total_collections"` 70 | FollowedByUser *bool `json:"followed_by_user"` 71 | NumericID *int `json:"numeric_id"` 72 | FollowersCount *int `json:"followers_count"` 73 | FollowingCount *int `json:"following_count"` 74 | Downloads *int `json:"downloads"` 75 | ProfileImage *ProfileImage `json:"profile_image"` 76 | Badge *UserBadge `json:"badge"` 77 | Links *UserLinks `json:"links,omitempty"` 78 | Photos *[]Photo `json:"photos"` 79 | UpdatedAt *string `json:"updated_at"` 80 | InstagramUsername *string `json:"instagram_username"` 81 | TwitterUsername *string `json:"twitter_username"` 82 | } 83 | 84 | func (u *User) String() string { 85 | var buf bytes.Buffer 86 | if u.ID == nil { 87 | return "User is not valid" 88 | } 89 | buf.WriteString("User: Name[") 90 | buf.WriteString(*u.Name) 91 | buf.WriteString("], ID[") 92 | buf.WriteString(*u.ID) 93 | buf.WriteString("]") 94 | return buf.String() 95 | } 96 | 97 | // UserUpdateInfo is used to update private data of a user 98 | type UserUpdateInfo struct { 99 | Username string `url:"username,omitempty"` 100 | FirstName string `url:"first_name,omitempty"` 101 | LastName string `url:"last_name,omitempty"` 102 | Bio string `url:"bio,omitempty"` 103 | Email string `url:"email,omitempty"` 104 | PortfolioURL string `url:"url,omitempty"` 105 | Location string `url:"location,omitempty"` 106 | InstagramUsername string `url:"instagram_username,omitempty"` 107 | } 108 | 109 | func (u *UserUpdateInfo) String() string { 110 | var buf bytes.Buffer 111 | buf.WriteString("UserUpdateInfo:") 112 | 113 | buf.WriteString("Username[") 114 | buf.WriteString(u.Username) 115 | buf.WriteString("]") 116 | 117 | buf.WriteString("FirstName[") 118 | buf.WriteString(u.FirstName) 119 | buf.WriteString("]") 120 | 121 | buf.WriteString("LastName[") 122 | buf.WriteString(u.LastName) 123 | buf.WriteString("]") 124 | 125 | buf.WriteString("Bio[") 126 | buf.WriteString(u.Bio) 127 | buf.WriteString("]") 128 | 129 | buf.WriteString("Email[") 130 | buf.WriteString(u.Email) 131 | buf.WriteString("]") 132 | 133 | buf.WriteString("PortfolioURL[") 134 | buf.WriteString(u.PortfolioURL) 135 | buf.WriteString("]") 136 | 137 | buf.WriteString("Location[") 138 | buf.WriteString(u.Location) 139 | buf.WriteString("]") 140 | 141 | buf.WriteString("InstagramUsername[") 142 | buf.WriteString(u.InstagramUsername) 143 | buf.WriteString("]") 144 | 145 | return buf.String() 146 | } 147 | 148 | // UserStatistics represents statistics like downloads, views and likes of an unsplash user 149 | type UserStatistics struct { 150 | Username string `json:"username"` 151 | Downloads struct { 152 | Total int `json:"total"` 153 | Historical struct { 154 | Change int `json:"change"` 155 | Average int `json:"average"` 156 | Resolution string `json:"resolution"` 157 | Quantity int `json:"quantity"` 158 | Values []struct { 159 | Date string `json:"date"` 160 | Value int `json:"value"` 161 | } `json:"values"` 162 | } `json:"historical"` 163 | } `json:"downloads"` 164 | Views struct { 165 | Total int `json:"total"` 166 | Historical struct { 167 | Change int `json:"change"` 168 | Average int `json:"average"` 169 | Resolution string `json:"resolution"` 170 | Quantity int `json:"quantity"` 171 | Values []struct { 172 | Date string `json:"date"` 173 | Value int `json:"value"` 174 | } `json:"values"` 175 | } `json:"historical"` 176 | } `json:"views"` 177 | Likes struct { 178 | Total int `json:"total"` 179 | Historical struct { 180 | Change int `json:"change"` 181 | Average int `json:"average"` 182 | Resolution string `json:"resolution"` 183 | Quantity int `json:"quantity"` 184 | Values []struct { 185 | Date string `json:"date"` 186 | Value int `json:"value"` 187 | } `json:"values"` 188 | } `json:"historical"` 189 | } `json:"likes"` 190 | } 191 | -------------------------------------------------------------------------------- /unsplash/users_service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "io/ioutil" 28 | "log" 29 | "testing" 30 | 31 | "github.com/jarcoal/httpmock" 32 | "github.com/stretchr/testify/assert" 33 | ) 34 | 35 | func TestUserProfile(T *testing.T) { 36 | T.Skip() 37 | assert := assert.New(T) 38 | log.SetOutput(ioutil.Discard) 39 | unsplash := setup() 40 | profileImageOpt := &ProfileImageOpt{Height: 120, Width: 400} 41 | user, err := unsplash.Users.User("lukechesser", profileImageOpt) 42 | assert.Nil(err) 43 | assert.NotNil(user) 44 | log.Println(user) 45 | assert.NotNil(user.Photos) 46 | photos := *user.Photos 47 | assert.NotNil(photos[0]) 48 | log.Println(len(photos)) 49 | photo := photos[0] 50 | assert.NotNil(&photo) 51 | pi := user.ProfileImage 52 | assert.NotNil(pi) 53 | assert.NotNil(pi.Medium) 54 | assert.NotNil(pi.Custom) 55 | log.Println(user.ProfileImage.Custom.String()) 56 | 57 | user, err = unsplash.Users.User("hbagdi", nil) 58 | assert.Nil(err) 59 | assert.NotNil(user) 60 | //log.Println(user) 61 | pi = user.ProfileImage 62 | assert.NotNil(pi) 63 | assert.Nil(pi.Custom) 64 | 65 | user, err = unsplash.Users.User("", nil) 66 | assert.Nil(user) 67 | assert.NotNil(err) 68 | iae, ok := err.(*IllegalArgumentError) 69 | assert.NotNil(iae) 70 | assert.Equal(true, ok) 71 | 72 | user, err = unsplash.Users.User(" batmanIsNotAuser", nil) 73 | assert.Nil(user) 74 | assert.NotNil(err) 75 | nfe, ok := err.(*NotFoundError) 76 | assert.NotNil(nfe) 77 | assert.Equal(true, ok) 78 | } 79 | 80 | func TestUserPortfolio(T *testing.T) { 81 | assert := assert.New(T) 82 | log.SetOutput(ioutil.Discard) 83 | unsplash := setup() 84 | url, err := unsplash.Users.Portfolio("hbagdi") 85 | assert.Nil(err) 86 | assert.NotNil(url) 87 | log.Println("URL is : ", url.String()) 88 | url, err = unsplash.Users.Portfolio("gopher") 89 | assert.Nil(err) 90 | assert.NotNil(url) 91 | assert.Equal(url.String(), "https://wikipedia.org/wiki/Gopher") 92 | log.Println("URL is : ", url.String()) 93 | 94 | url, err = unsplash.Users.Portfolio("") 95 | assert.Nil(url) 96 | assert.NotNil(err) 97 | iae, ok := err.(*IllegalArgumentError) 98 | assert.NotNil(iae) 99 | assert.Equal(true, ok) 100 | } 101 | 102 | func TestUserStatistics(T *testing.T) { 103 | assert := assert.New(T) 104 | log.SetOutput(ioutil.Discard) 105 | unsplash := setup() 106 | stats, resp, err := unsplash.Users.Statistics("lukechesser", nil) 107 | assert.Nil(err) 108 | assert.NotNil(stats) 109 | assert.NotNil(resp) 110 | assert.NotNil(30, stats.Downloads.Historical.Quantity) 111 | log.Println(stats) 112 | 113 | stats, resp, err = unsplash.Users.Statistics("lukechesser", &StatsOpt{Quantity: 10}) 114 | assert.Nil(err) 115 | assert.NotNil(stats) 116 | assert.NotNil(resp) 117 | assert.NotNil(30, stats.Downloads.Historical.Quantity) 118 | log.Println(stats) 119 | 120 | stats, resp, err = unsplash.Users.Statistics("lukechesser", &StatsOpt{Resolution: "sd"}) 121 | assert.NotNil(err) 122 | assert.Nil(resp) 123 | 124 | stats, resp, err = unsplash.Users.Statistics("lukechesser", &StatsOpt{Quantity: 31}) 125 | assert.NotNil(err) 126 | assert.Nil(resp) 127 | 128 | stats, resp, err = unsplash.Users.Statistics("", nil) 129 | assert.Nil(stats) 130 | assert.Nil(resp) 131 | assert.NotNil(err) 132 | } 133 | func TestLikedPhotos(T *testing.T) { 134 | assert := assert.New(T) 135 | log.SetOutput(ioutil.Discard) 136 | unsplash := setup() 137 | // hopefully cofounder won't change his username 138 | photos, resp, err := unsplash.Users.LikedPhotos("lukechesser", nil) 139 | assert.Nil(err) 140 | //check pagination 141 | assert.NotNil(resp) 142 | log.Println(resp) 143 | assert.Equal(true, resp.HasNextPage) 144 | assert.Equal(2, resp.NextPage) 145 | // lastPage := resp.LastPage 146 | //check photos 147 | assert.NotNil(photos) 148 | assert.Equal(10, len(*photos)) 149 | 150 | opt := *defaultListOpt 151 | opt.Page = 2 152 | opt.PerPage = 42 153 | photos, resp, err = unsplash.Users.LikedPhotos("lukechesser", &opt) 154 | assert.Nil(err) 155 | log.Println(err) 156 | assert.NotNil(resp) 157 | log.Println(resp) 158 | assert.Equal(true, resp.HasNextPage) 159 | assert.Equal(3, resp.NextPage) 160 | assert.Equal(1, resp.PrevPage) 161 | assert.NotNil(photos) 162 | assert.Equal(30, len(*photos)) 163 | 164 | photos, resp, err = unsplash.Users.LikedPhotos("lukechesser", &ListOpt{PerPage: -1}) 165 | assert.Nil(photos) 166 | assert.Nil(resp) 167 | assert.NotNil(err) 168 | _, ok := err.(*InvalidListOptError) 169 | assert.Equal(true, ok) 170 | 171 | photos, resp, err = unsplash.Users.LikedPhotos("", nil) 172 | assert.NotNil(err) 173 | assert.Nil(photos) 174 | assert.Nil(resp) 175 | } 176 | func TestUserPhotos(T *testing.T) { 177 | assert := assert.New(T) 178 | //TODO write better tests 179 | log.SetOutput(ioutil.Discard) 180 | unsplash := setup() 181 | // hopefully cofounder won't change his username 182 | _, resp, err := unsplash.Users.Photos("lukechesser", nil) 183 | assert.Nil(err) 184 | //check pagination 185 | assert.NotNil(resp) 186 | log.Println(resp) 187 | 188 | photos, resp, err := unsplash.Users.Photos("", nil) 189 | assert.NotNil(err) 190 | assert.Nil(photos) 191 | assert.Nil(resp) 192 | 193 | } 194 | func TestUserCollections(T *testing.T) { 195 | T.Skip() 196 | assert := assert.New(T) 197 | //TODO write better tests 198 | log.SetOutput(ioutil.Discard) 199 | unsplash := setup() 200 | // hopefully cofounder won't change his username 201 | _, resp, err := unsplash.Users.Collections("gopher", nil) 202 | assert.Nil(err) 203 | //check pagination 204 | assert.NotNil(resp) 205 | log.Println(resp) 206 | 207 | photos, resp, err := unsplash.Users.Collections("", nil) 208 | assert.NotNil(err) 209 | assert.Nil(photos) 210 | assert.Nil(resp) 211 | } 212 | 213 | func rogueUserServiceTests(T *testing.T, responder httpmock.Responder) { 214 | 215 | httpmock.Activate() 216 | defer httpmock.DeactivateAndReset() 217 | log.SetOutput(ioutil.Discard) 218 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(users)+"/gopher", 219 | responder) 220 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(users)+"/gopher/portfolio", 221 | responder) 222 | 223 | unsplash := setup() 224 | assert := assert.New(T) 225 | user, err := unsplash.Users.User("gopher", nil) 226 | assert.Nil(user) 227 | assert.NotNil(err) 228 | log.Println(err) 229 | 230 | url, err := unsplash.Users.Portfolio("gopher") 231 | assert.Nil(url) 232 | assert.NotNil(err) 233 | log.Println(err) 234 | } 235 | 236 | func TestUserServiceRogueStuff(T *testing.T) { 237 | rogueUserServiceTests(T, httpmock.NewStringResponder(200, `Bad ass Bug flow`)) 238 | rogueUserServiceTests(T, nil) 239 | } 240 | -------------------------------------------------------------------------------- /unsplash/collections_service.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "encoding/json" 28 | "errors" 29 | "fmt" 30 | ) 31 | 32 | // CollectionsService interacts with /users endpoint 33 | type CollectionsService service 34 | 35 | // All returns a list of all collections on unsplash. 36 | // Note that some fields in collection structs from this result will be missing. 37 | // Use Collection() method to get all details of the Collection. 38 | func (cs *CollectionsService) All(opt *ListOpt) (*[]Collection, *Response, error) { 39 | s := (service)(*cs) 40 | return s.getCollections(opt, getEndpoint(collections)) 41 | } 42 | 43 | // Featured returns a list of featured collections on unsplash. 44 | // Note that some fields in collection structs from this result will be missing. 45 | // Use Collection() method to get all details of the Collection. 46 | func (cs *CollectionsService) Featured(opt *ListOpt) (*[]Collection, *Response, error) { 47 | s := (service)(*cs) 48 | return s.getCollections(opt, getEndpoint(collections)+"/featured") 49 | } 50 | 51 | // Curated returns a list of curated collections on unsplash. 52 | // Note that some fields in collection structs from this result will be missing. 53 | // Use Collection() method to get all details of the Collection. 54 | func (cs *CollectionsService) Curated(opt *ListOpt) (*[]Collection, *Response, error) { 55 | s := (service)(*cs) 56 | return s.getCollections(opt, getEndpoint(collections)+"/curated") 57 | } 58 | 59 | // Related returns a list of collections related to collections with id. 60 | func (cs *CollectionsService) Related(id string, opt *ListOpt) (*[]Collection, *Response, error) { 61 | if "" == id { 62 | return nil, nil, &IllegalArgumentError{ErrString: "Collection ID cannot be nil"} 63 | } 64 | s := (service)(*cs) 65 | endpoint := fmt.Sprintf("%v/%v/%v", getEndpoint(collections), id, "related") 66 | return s.getCollections(opt, endpoint) 67 | } 68 | 69 | // Collection returns a collection with id. 70 | func (cs *CollectionsService) Collection(id string) (*Collection, *Response, error) { 71 | if "" == id { 72 | return nil, nil, &IllegalArgumentError{ErrString: "Collection ID cannot be nil"} 73 | } 74 | endpoint := fmt.Sprintf("%v/%v", getEndpoint(collections), id) 75 | req, err := newRequest(GET, endpoint, nil, nil) 76 | if err != nil { 77 | return nil, nil, err 78 | } 79 | resp, err := cs.client.do(req) 80 | if err != nil { 81 | return nil, nil, err 82 | } 83 | var collection Collection 84 | err = json.Unmarshal(*resp.body, &collection) 85 | if err != nil { 86 | return nil, nil, err 87 | } 88 | return &collection, resp, nil 89 | } 90 | 91 | //CollectionOpt shows various available optional parameters available 92 | //during creatioin of collection 93 | type CollectionOpt struct { 94 | Title *string `url:"title,omitempty"` 95 | Description *string `url:"description,omitempty"` 96 | Private *bool `url:"private,omitempty"` 97 | } 98 | 99 | //Create creates a new collection on the authenticated user's profile. 100 | func (cs *CollectionsService) Create(opt *CollectionOpt) (*Collection, *Response, error) { 101 | if nil == opt { 102 | return nil, nil, &IllegalArgumentError{ErrString: "Opt cannot be nil"} 103 | } 104 | if *opt.Title == "" { 105 | return nil, nil, &IllegalArgumentError{ErrString: "Need to provide a title for the new collection."} 106 | } 107 | req, err := newRequest(POST, getEndpoint(collections), opt, nil) 108 | if err != nil { 109 | return nil, nil, err 110 | } 111 | resp, err := cs.client.do(req) 112 | if err != nil { 113 | return nil, nil, err 114 | } 115 | var collection Collection 116 | err = json.Unmarshal(*resp.body, &collection) 117 | if err != nil { 118 | return nil, nil, err 119 | } 120 | if resp.httpResponse.StatusCode != 201 { 121 | return nil, nil, errors.New("failed to create the collection") 122 | } 123 | return &collection, resp, nil 124 | } 125 | 126 | //Update updates an existing collection on the authenticated user's profile. 127 | func (cs *CollectionsService) Update(collectionID int, opt *CollectionOpt) (*Collection, *Response, error) { 128 | if nil == opt { 129 | return nil, nil, &IllegalArgumentError{ErrString: "Opt cannot be nil"} 130 | } 131 | if collectionID == 0 { 132 | return nil, nil, &IllegalArgumentError{ErrString: "collectionID cannot be nil."} 133 | } 134 | endpoint := fmt.Sprintf("%v/%v", getEndpoint(collections), collectionID) 135 | req, err := newRequest(PUT, endpoint, opt, nil) 136 | if err != nil { 137 | return nil, nil, err 138 | } 139 | resp, err := cs.client.do(req) 140 | if err != nil { 141 | return nil, nil, err 142 | } 143 | var collection Collection 144 | err = json.Unmarshal(*resp.body, &collection) 145 | if err != nil { 146 | return nil, nil, err 147 | } 148 | return &collection, resp, nil 149 | } 150 | 151 | //Delete deletes a collection on the authenticated user's profile. 152 | func (cs *CollectionsService) Delete(collectionID int) (*Response, error) { 153 | if collectionID == 0 { 154 | return nil, &IllegalArgumentError{ErrString: "CollectionID cannot be empty or zero."} 155 | } 156 | endpoint := fmt.Sprintf("%v/%v", getEndpoint(collections), collectionID) 157 | req, err := newRequest(DELETE, endpoint, nil, nil) 158 | if err != nil { 159 | return nil, err 160 | } 161 | resp, err := cs.client.do(req) 162 | if err != nil { 163 | return nil, err 164 | } 165 | if resp.httpResponse.StatusCode != 204 { 166 | return nil, errors.New("failed to delete the collection") 167 | } 168 | return resp, nil 169 | } 170 | 171 | type addPhoto struct { 172 | Photo string `url:"photo_id"` 173 | } 174 | 175 | //AddPhoto adds a photo to a collection owned by an authenticated user. 176 | func (cs *CollectionsService) AddPhoto(collectionID int, photoID string) (*Response, error) { 177 | if collectionID == 0 { 178 | return nil, &IllegalArgumentError{ErrString: "CollectionID cannot be empty or zero."} 179 | } 180 | if photoID == "" { 181 | return nil, &IllegalArgumentError{ErrString: "PhotoID cannot be empty or zero."} 182 | } 183 | opt := &addPhoto{photoID} 184 | endpoint := fmt.Sprintf("%v/%v/add", getEndpoint(collections), collectionID) 185 | req, err := newRequest(POST, endpoint, opt, nil) 186 | if err != nil { 187 | return nil, err 188 | } 189 | resp, err := cs.client.do(req) 190 | if err != nil { 191 | return nil, err 192 | } 193 | if resp.httpResponse.StatusCode != 201 { 194 | return nil, errors.New("failed to add photo to the collection") 195 | } 196 | return resp, nil 197 | } 198 | 199 | //RemovePhoto removes a photo from a collection owned by an authenticated user. 200 | func (cs *CollectionsService) RemovePhoto(collectionID int, photoID string) (*Response, error) { 201 | if collectionID == 0 { 202 | return nil, &IllegalArgumentError{ErrString: "CollectionID cannot be empty or zero."} 203 | } 204 | if photoID == "" { 205 | return nil, &IllegalArgumentError{ErrString: "PhotoID cannot be empty or zero."} 206 | } 207 | opt := &addPhoto{photoID} 208 | endpoint := fmt.Sprintf("%v/%v/remove", getEndpoint(collections), collectionID) 209 | req, err := newRequest(DELETE, endpoint, opt, nil) 210 | if err != nil { 211 | return nil, err 212 | } 213 | resp, err := cs.client.do(req) 214 | if err != nil { 215 | return nil, err 216 | } 217 | if resp.httpResponse.StatusCode != 200 { 218 | return nil, errors.New("failed to remove photo from the collection") 219 | } 220 | return resp, nil 221 | } 222 | -------------------------------------------------------------------------------- /unsplash/stats.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "bytes" 28 | "encoding/json" 29 | "strconv" 30 | "strings" 31 | ) 32 | 33 | // GlobalStats shows the total photo stats of Unsplash.com 34 | type GlobalStats struct { 35 | TotalPhotos uint64 36 | PhotoDownloads uint64 37 | Photos uint64 `json:"photos,omitempty"` 38 | Downloads uint64 `json:"downloads,omitempty"` 39 | Views uint64 `json:"views,omitempty"` 40 | Likes uint64 `json:"likes,omitempty"` 41 | Photographers uint64 `json:"photographers,omitempty"` 42 | Pixels uint64 `json:"pixels,omitempty"` 43 | DownloadsPerSecond uint64 `json:"downloads_per_second,omitempty"` 44 | ViewsPerSecond uint64 `json:"views_per_second,omitempty"` 45 | Developers uint64 `json:"developers,omitempty"` 46 | Applications uint64 `json:"applications,omitempty"` 47 | Requests uint64 `json:"requests,omitempty"` 48 | } 49 | 50 | // processNumber converts a string or float or int representation into an int 51 | // this hack is needed because the API strangely returns a float value in quotes in the JSON response for this endpoint 52 | func processNumber(i interface{}) uint64 { 53 | var n uint64 54 | switch v := i.(type) { 55 | case uint64: 56 | n = v 57 | case string: 58 | s := strings.Split(v, ".") 59 | n, _ = strconv.ParseUint(s[0], 10, 64) 60 | case float64: 61 | n = uint64(v) 62 | } 63 | return n 64 | } 65 | 66 | // UnmarshalJSON converts a JSON string representation of GlobalStats into a struct 67 | func (gs *GlobalStats) UnmarshalJSON(b []byte) error { 68 | var f interface{} 69 | err := json.Unmarshal(b, &f) 70 | if err != nil { 71 | return err 72 | } 73 | m := f.(map[string]interface{}) 74 | if v, ok := m["photos"]; ok { 75 | gs.Photos = processNumber(v) 76 | gs.TotalPhotos = gs.Photos 77 | } 78 | if v, ok := m["downloads"]; ok { 79 | gs.Downloads = processNumber(v) 80 | gs.PhotoDownloads = gs.Downloads 81 | } 82 | if v, ok := m["views"]; ok { 83 | gs.Views = processNumber(v) 84 | } 85 | if v, ok := m["likes"]; ok { 86 | gs.Likes = processNumber(v) 87 | } 88 | if v, ok := m["photographers"]; ok { 89 | gs.Photographers = processNumber(v) 90 | } 91 | if v, ok := m["pixels"]; ok { 92 | gs.Pixels = processNumber(v) 93 | } 94 | if v, ok := m["downloads_per_second"]; ok { 95 | gs.DownloadsPerSecond = processNumber(v) 96 | } 97 | if v, ok := m["views_per_second"]; ok { 98 | gs.ViewsPerSecond = processNumber(v) 99 | } 100 | if v, ok := m["developers"]; ok { 101 | gs.Developers = processNumber(v) 102 | } 103 | if v, ok := m["applications"]; ok { 104 | gs.Applications = processNumber(v) 105 | } 106 | if v, ok := m["requests"]; ok { 107 | gs.Requests = processNumber(v) 108 | } 109 | return nil 110 | } 111 | func (gs *GlobalStats) String() string { 112 | var buf bytes.Buffer 113 | buf.WriteString("Global Stats: ") 114 | 115 | buf.WriteString(" Photos[") 116 | buf.WriteString(strconv.FormatUint(gs.Photos, 10)) 117 | buf.WriteString("]") 118 | 119 | buf.WriteString(" Downloads[") 120 | buf.WriteString(strconv.FormatUint(gs.Downloads, 10)) 121 | buf.WriteString("]") 122 | 123 | buf.WriteString(" Views[") 124 | buf.WriteString(strconv.FormatUint(gs.Views, 10)) 125 | buf.WriteString("]") 126 | 127 | buf.WriteString(" Likes[") 128 | buf.WriteString(strconv.FormatUint(gs.Likes, 10)) 129 | buf.WriteString("]") 130 | 131 | buf.WriteString(" Photographers[") 132 | buf.WriteString(strconv.FormatUint(gs.Photographers, 10)) 133 | buf.WriteString("]") 134 | 135 | buf.WriteString(" Pixels[") 136 | buf.WriteString(strconv.FormatUint(gs.Pixels, 10)) 137 | buf.WriteString("]") 138 | 139 | buf.WriteString(" DownloadsPerSecond[") 140 | buf.WriteString(strconv.FormatUint(gs.DownloadsPerSecond, 10)) 141 | buf.WriteString("]") 142 | 143 | buf.WriteString(" ViewsPerSecond[") 144 | buf.WriteString(strconv.FormatUint(gs.ViewsPerSecond, 10)) 145 | buf.WriteString("]") 146 | 147 | buf.WriteString(" Developers[") 148 | buf.WriteString(strconv.FormatUint(gs.Developers, 10)) 149 | buf.WriteString("]") 150 | 151 | buf.WriteString(" Applications[") 152 | buf.WriteString(strconv.FormatUint(gs.Applications, 10)) 153 | buf.WriteString("]") 154 | 155 | buf.WriteString(" Requests[") 156 | buf.WriteString(strconv.FormatUint(gs.Requests, 10)) 157 | buf.WriteString("]") 158 | return buf.String() 159 | } 160 | 161 | // MonthStats shows the overall Unsplash stats for the past 30 days. 162 | type MonthStats struct { 163 | Downloads uint64 `json:"downloads"` 164 | Views uint64 `json:"views"` 165 | Likes uint64 `json:"likes"` 166 | NewPhotos uint64 `json:"new_photos"` 167 | NewPhotographers uint64 `json:"new_photographers"` 168 | NewPixels uint64 `json:"new_pixels"` 169 | NewDevelopers uint64 `json:"new_developers"` 170 | NewApplications uint64 `json:"new_applications"` 171 | NewRequests uint64 `json:"new_requests"` 172 | } 173 | 174 | // UnmarshalJSON converts a JSON string representation of GlobalStats into a struct 175 | func (stats *MonthStats) UnmarshalJSON(b []byte) error { 176 | var f interface{} 177 | err := json.Unmarshal(b, &f) 178 | if err != nil { 179 | return err 180 | } 181 | m := f.(map[string]interface{}) 182 | if v, ok := m["downloads"]; ok { 183 | stats.Downloads = processNumber(v) 184 | } 185 | if v, ok := m["views"]; ok { 186 | stats.Views = processNumber(v) 187 | } 188 | if v, ok := m["likes"]; ok { 189 | stats.Likes = processNumber(v) 190 | } 191 | if v, ok := m["new_photographers"]; ok { 192 | stats.NewPhotographers = processNumber(v) 193 | } 194 | if v, ok := m["new_pixels"]; ok { 195 | stats.NewPixels = processNumber(v) 196 | } 197 | if v, ok := m["new_photos"]; ok { 198 | stats.NewPhotos = processNumber(v) 199 | } 200 | if v, ok := m["new_requests"]; ok { 201 | stats.NewRequests = processNumber(v) 202 | } 203 | if v, ok := m["new_developers"]; ok { 204 | stats.NewDevelopers = processNumber(v) 205 | } 206 | if v, ok := m["new_applications"]; ok { 207 | stats.NewApplications = processNumber(v) 208 | } 209 | return nil 210 | } 211 | func (stats *MonthStats) String() string { 212 | var buf bytes.Buffer 213 | buf.WriteString("Monthly Stats: ") 214 | 215 | buf.WriteString(" Downloads[") 216 | buf.WriteString(strconv.FormatUint(stats.Downloads, 10)) 217 | buf.WriteString("]") 218 | 219 | buf.WriteString(" Views[") 220 | buf.WriteString(strconv.FormatUint(stats.Views, 10)) 221 | buf.WriteString("]") 222 | 223 | buf.WriteString(" Likes[") 224 | buf.WriteString(strconv.FormatUint(stats.Likes, 10)) 225 | buf.WriteString("]") 226 | 227 | buf.WriteString(" New Photos[") 228 | buf.WriteString(strconv.FormatUint(stats.NewPhotos, 10)) 229 | buf.WriteString("]") 230 | 231 | buf.WriteString(" New Photographers[") 232 | buf.WriteString(strconv.FormatUint(stats.NewPhotographers, 10)) 233 | buf.WriteString("]") 234 | 235 | buf.WriteString(" New Pixels[") 236 | buf.WriteString(strconv.FormatUint(stats.NewPixels, 10)) 237 | buf.WriteString("]") 238 | 239 | buf.WriteString(" New Applications[") 240 | buf.WriteString(strconv.FormatUint(stats.NewApplications, 10)) 241 | buf.WriteString("]") 242 | 243 | buf.WriteString(" New Developers[") 244 | buf.WriteString(strconv.FormatUint(stats.NewDevelopers, 10)) 245 | buf.WriteString("]") 246 | 247 | buf.WriteString(" New Requests[") 248 | buf.WriteString(strconv.FormatUint(stats.NewRequests, 10)) 249 | buf.WriteString("]") 250 | 251 | return buf.String() 252 | } 253 | -------------------------------------------------------------------------------- /unsplash/unsplash_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "encoding/json" 28 | "fmt" 29 | "io/ioutil" 30 | "log" 31 | "math/rand" 32 | "net/http" 33 | "os" 34 | "testing" 35 | "time" 36 | 37 | "github.com/jarcoal/httpmock" 38 | "github.com/stretchr/testify/assert" 39 | "golang.org/x/oauth2" 40 | ) 41 | 42 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 43 | 44 | type AuthConfig struct { 45 | AuthToken string 46 | } 47 | 48 | func authFromFile() *AuthConfig { 49 | bytes, err := ioutil.ReadFile("auth.json") 50 | if err != nil { 51 | return nil 52 | } 53 | var config AuthConfig 54 | err = json.Unmarshal(bytes, &config) 55 | if err != nil { 56 | return nil 57 | } 58 | return &config 59 | } 60 | 61 | func randName(n int) string { 62 | b := make([]rune, n) 63 | for i := range b { 64 | b[i] = letters[rand.Intn(len(letters))] 65 | } 66 | return string(b) 67 | } 68 | 69 | func getUserAuth() *AuthConfig { 70 | token, ok := os.LookupEnv("UNSPLASH_USERTOKEN") 71 | fmt.Println(token) 72 | if !ok { 73 | return nil 74 | } 75 | return &AuthConfig{ 76 | AuthToken: token, 77 | } 78 | } 79 | 80 | func setup() *Unsplash { 81 | var c *AuthConfig 82 | c = getUserAuth() 83 | if c == nil { 84 | c = authFromFile() 85 | if c == nil { 86 | log.Println("Couldn't read auth token. Stopping tests.") 87 | os.Exit(1) 88 | } 89 | } 90 | ts := oauth2.StaticTokenSource( 91 | &oauth2.Token{AccessToken: c.AuthToken}, 92 | ) 93 | client := oauth2.NewClient(oauth2.NoContext, ts) 94 | return New(client) 95 | } 96 | func TestUnsplash(T *testing.T) { 97 | T.Skip() 98 | assert := assert.New(T) 99 | log.SetOutput(ioutil.Discard) 100 | unsplash := setup() 101 | assert.NotNil(unsplash) 102 | assert.NotNil(unsplash.common) 103 | assert.NotNil(unsplash.client) 104 | tstats, resp, err := unsplash.TotalStats() 105 | assert.Nil(err) 106 | assert.NotNil(tstats) 107 | assert.NotNil(resp) 108 | stats, resp, err := unsplash.Stats() 109 | assert.Nil(err) 110 | assert.NotNil(stats) 111 | //FIXME 112 | if stats.Photos <= 0 || stats.Downloads <= 0 || stats.Views <= 0 || stats.Likes <= 0 || stats.Photographers <= 0 || stats.Pixels <= 0 || stats.DownloadsPerSecond <= 0 || stats.ViewsPerSecond <= 0 || stats.Developers <= 0 || stats.Applications <= 0 || stats.Requests <= 0 { 113 | assert.Fail("GlobalStats struct has a zero field: %s\n", stats.String()) 114 | } 115 | assert.NotNil(resp) 116 | log.Println(stats) 117 | 118 | //Disabling the test since API almost always times out 119 | // monthlyStats, resp, err := unsplash.MonthStats() 120 | // assert.Nil(err) 121 | // assert.NotNil(resp) 122 | // assert.NotNil(monthlyStats) 123 | 124 | unsplash = New(nil) 125 | stats, resp, err = unsplash.Stats() 126 | assert.Nil(stats) 127 | assert.Nil(resp) 128 | assert.NotNil(err) 129 | 130 | var s service 131 | _, err = s.client.do(nil) 132 | assert.NotNil(err) 133 | } 134 | 135 | func TestUnsplashRogueServer(T *testing.T) { 136 | httpmock.Activate() 137 | defer httpmock.DeactivateAndReset() 138 | 139 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(currentUser), 140 | httpmock.NewStringResponder(200, `Bad ass Bug flow`)) 141 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(globalStats), 142 | httpmock.NewStringResponder(200, `Bad ass Bug flow`)) 143 | unsplash := setup() 144 | assert := assert.New(T) 145 | user, resp, err := unsplash.CurrentUser() 146 | assert.Nil(user) 147 | assert.Nil(resp) 148 | assert.NotNil(err) 149 | log.SetOutput(ioutil.Discard) 150 | log.Println(err) 151 | stats, resp, err := unsplash.Stats() 152 | assert.Nil(stats) 153 | assert.Nil(resp) 154 | assert.NotNil(err) 155 | } 156 | 157 | func TestUnsplashRogueNetwork(T *testing.T) { 158 | httpmock.Activate() 159 | defer httpmock.DeactivateAndReset() 160 | 161 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(currentUser), 162 | nil) 163 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(globalStats), 164 | nil) 165 | unsplash := setup() 166 | assert := assert.New(T) 167 | user, resp, err := unsplash.CurrentUser() 168 | assert.Nil(user) 169 | assert.Nil(resp) 170 | assert.NotNil(err) 171 | log.SetOutput(ioutil.Discard) 172 | log.Println(err) 173 | stats, resp, err := unsplash.Stats() 174 | assert.Nil(stats) 175 | assert.Nil(resp) 176 | assert.NotNil(err) 177 | } 178 | 179 | func TestUpdateCurrentUser(T *testing.T) { 180 | log.SetOutput(ioutil.Discard) 181 | assert := assert.New(T) 182 | unsplash := setup() 183 | assert.NotNil(unsplash) 184 | newUserName := "lukechesser" 185 | 186 | user, resp, err := unsplash.UpdateCurrentUser(nil) 187 | assert.NotNil(err) 188 | assert.Nil(user) 189 | assert.Nil(resp) 190 | log.Println(err.Error()) 191 | 192 | user, resp, err = unsplash.UpdateCurrentUser(&UserUpdateInfo{Username: newUserName}) 193 | assert.NotNil(err) 194 | assert.Nil(user) 195 | assert.Nil(resp) 196 | log.Println(err.Error()) 197 | } 198 | 199 | func TestClientIDPropagation(T *testing.T) { 200 | httpmock.Activate() 201 | defer httpmock.DeactivateAndReset() 202 | 203 | expected_client_id := "HARDCODED_TEST" 204 | 205 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(searchPhotos), 206 | func(r *http.Request) (*http.Response, error) { 207 | client_id_header := r.Header.Get("Authorization") 208 | 209 | assert.Equal(T, fmt.Sprintf("Client-ID %v", expected_client_id), client_id_header) 210 | 211 | return httpmock.NewStringResponse(200, `{"total":1,"total_pages":1,"results":[{"id":"eOLpJytrbsQ","created_at":"2014-11-18T14:35:36-05:00","width":4000,"height":3000,"color":"#A7A2A1","blur_hash":"LaLXMa9Fx[D%~q%MtQM|kDRjtRIU","likes":286,"liked_by_user":false,"description":"A man drinking a coffee.","user":{"id":"Ul0QVz12Goo","username":"ugmonk","name":"Jeff Sheldon","first_name":"Jeff","last_name":"Sheldon","instagram_username":"instantgrammer","twitter_username":"ugmonk","portfolio_url":"http://ugmonk.com/","profile_image":{"small":"https://images.unsplash.com/profile-1441298803695-accd94000cac?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=7cfe3b93750cb0c93e2f7caec08b5a41","medium":"https://images.unsplash.com/profile-1441298803695-accd94000cac?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=5a9dc749c43ce5bd60870b129a40902f","large":"https://images.unsplash.com/profile-1441298803695-accd94000cac?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=32085a077889586df88bfbe406692202"},"links":{"self":"https://api.unsplash.com/users/ugmonk","html":"http://unsplash.com/@ugmonk","photos":"https://api.unsplash.com/users/ugmonk/photos","likes":"https://api.unsplash.com/users/ugmonk/likes"}},"current_user_collections":[],"urls":{"raw":"https://images.unsplash.com/photo-1416339306562-f3d12fefd36f","full":"https://hd.unsplash.com/photo-1416339306562-f3d12fefd36f","regular":"https://images.unsplash.com/photo-1416339306562-f3d12fefd36f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&s=92f3e02f63678acc8416d044e189f515","small":"https://images.unsplash.com/photo-1416339306562-f3d12fefd36f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&s=263af33585f9d32af39d165b000845eb","thumb":"https://images.unsplash.com/photo-1416339306562-f3d12fefd36f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&s=8aae34cf35df31a592f0bef16e6342ef"},"links":{"self":"https://api.unsplash.com/photos/eOLpJytrbsQ","html":"http://unsplash.com/photos/eOLpJytrbsQ","download":"http://unsplash.com/photos/eOLpJytrbsQ/download"}}]}`), nil 212 | }) 213 | 214 | u := NewWithClientID(&http.Client{Timeout: time.Duration(1) * time.Second}, expected_client_id) 215 | 216 | opt := SearchOpt{ 217 | Page: 1, 218 | PerPage: 1, 219 | Query: "nowhere", 220 | } 221 | photo, _, err := u.Search.Photos(&opt) 222 | assert.Equal(T, nil, err) 223 | assert.NotEqual(T, nil, photo) 224 | 225 | info := httpmock.GetCallCountInfo() 226 | assert.Equal(T, 1, info[fmt.Sprintf("GET %v%v", getEndpoint(base), getEndpoint(searchPhotos))]) 227 | } 228 | -------------------------------------------------------------------------------- /unsplash/photos_service.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "bytes" 28 | "encoding/json" 29 | "fmt" 30 | "strconv" 31 | ) 32 | 33 | // PhotosService interacts with /photos endpoint 34 | type PhotosService service 35 | 36 | // PhotoOpt denotes properties of any Image 37 | type PhotoOpt struct { 38 | Height int `json:"h" url:"h"` 39 | Width int `json:"w" url:"w"` 40 | CropX int 41 | CropY int 42 | Crop bool 43 | } 44 | 45 | // Valid validates a PhotoOpt 46 | func (p *PhotoOpt) Valid() bool { 47 | if p.Height <= 0 || p.Width <= 0 || p.CropX < 0 || p.CropY < 0 { 48 | return false 49 | } 50 | return true 51 | } 52 | 53 | type rect struct { 54 | Rect string `url:"rect"` 55 | } 56 | 57 | func processPhotoOpt(photoOpt *PhotoOpt) interface{} { 58 | if !photoOpt.Crop { 59 | return photoOpt 60 | } 61 | var buf bytes.Buffer 62 | var r rect 63 | buf.WriteString(strconv.Itoa(photoOpt.CropX) + "," + 64 | strconv.Itoa(photoOpt.CropY) + "," + 65 | strconv.Itoa(photoOpt.Width) + "," + 66 | strconv.Itoa(photoOpt.Height)) 67 | r.Rect = buf.String() 68 | return r 69 | } 70 | 71 | // Photo return a photo with id 72 | func (ps *PhotosService) Photo(id string, photoOpt *PhotoOpt) (*Photo, *Response, error) { 73 | if "" == id { 74 | return nil, nil, &IllegalArgumentError{ErrString: "Photo ID cannot be null"} 75 | } 76 | 77 | // Validation and conversion if necessary of photoOpt 78 | var opt interface{} 79 | opt = nil 80 | if photoOpt != nil { 81 | if !photoOpt.Valid() { 82 | return nil, nil, &InvalidPhotoOptError{ErrString: " photoOpt has zero or non-negative values"} 83 | } 84 | opt = processPhotoOpt(photoOpt) 85 | } 86 | endpoint := fmt.Sprintf("%v/%v", getEndpoint(photos), id) 87 | req, err := newRequest(GET, endpoint, opt, nil) 88 | if err != nil { 89 | return nil, nil, err 90 | } 91 | resp, err := ps.client.do(req) 92 | if err != nil { 93 | return nil, nil, err 94 | } 95 | var photo Photo 96 | err = json.Unmarshal(*resp.body, &photo) 97 | if err != nil { 98 | return nil, nil, err 99 | } 100 | return &photo, resp, nil 101 | } 102 | 103 | // Stats return a stats about a photo with id. 104 | func (ps *PhotosService) Stats(id string) (*PhotoStats, *Response, error) { 105 | if "" == id { 106 | return nil, nil, &IllegalArgumentError{ErrString: "Photo ID cannot be null"} 107 | } 108 | endpoint := fmt.Sprintf("%v/%v/stats", getEndpoint(photos), id) 109 | req, err := newRequest(GET, endpoint, nil, nil) 110 | if err != nil { 111 | return nil, nil, err 112 | } 113 | resp, err := ps.client.do(req) 114 | if err != nil { 115 | return nil, nil, err 116 | } 117 | var stats PhotoStats 118 | err = json.Unmarshal(*resp.body, &stats) 119 | if err != nil { 120 | return nil, nil, err 121 | } 122 | return &stats, resp, nil 123 | } 124 | 125 | // Statistics return a stats about a photo with id. 126 | func (ps *PhotosService) Statistics(id string, opt *StatsOpt) (*PhotoStatistics, *Response, error) { 127 | if "" == id { 128 | return nil, nil, &IllegalArgumentError{ErrString: "Photo ID cannot be null"} 129 | } 130 | if opt == nil { 131 | opt = defaultStatsOpt 132 | } 133 | if !opt.Valid() { 134 | return nil, nil, &InvalidStatsOptError{ErrString: "opt provided is not valid."} 135 | } 136 | endpoint := fmt.Sprintf("%v/%v/statistics", getEndpoint(photos), id) 137 | req, err := newRequest(GET, endpoint, opt, nil) 138 | if err != nil { 139 | return nil, nil, err 140 | } 141 | resp, err := ps.client.do(req) 142 | if err != nil { 143 | return nil, nil, err 144 | } 145 | var stats PhotoStatistics 146 | err = json.Unmarshal(*resp.body, &stats) 147 | if err != nil { 148 | return nil, nil, err 149 | } 150 | return &stats, resp, nil 151 | } 152 | 153 | // DownloadLink return the download URL for a photo. 154 | func (ps *PhotosService) DownloadLink(id string) (*URL, *Response, error) { 155 | if "" == id { 156 | return nil, nil, &IllegalArgumentError{ErrString: "Photo ID cannot be null"} 157 | } 158 | endpoint := fmt.Sprintf("%v/%v/download", getEndpoint(photos), id) 159 | req, err := newRequest(GET, endpoint, nil, nil) 160 | if err != nil { 161 | return nil, nil, err 162 | } 163 | resp, err := ps.client.do(req) 164 | if err != nil { 165 | return nil, nil, err 166 | } 167 | var url urlWrapper 168 | err = json.Unmarshal(*resp.body, &url) 169 | if err != nil { 170 | return nil, nil, err 171 | } 172 | return url.URL, resp, nil 173 | } 174 | 175 | // All returns a list of all photos on unsplash. 176 | // Note that some fields in photo structs from this result will be missing. 177 | // Use Photo() method to get all details of the Photo. 178 | func (ps *PhotosService) All(listOpt *ListOpt) (*[]Photo, *Response, error) { 179 | s := (service)(*ps) 180 | return s.getPhotos(listOpt, getEndpoint(photos)) 181 | } 182 | 183 | // Curated return a list of all curated photos. 184 | func (ps *PhotosService) Curated(listOpt *ListOpt) (*[]Photo, *Response, error) { 185 | s := (service)(*ps) 186 | return s.getPhotos(listOpt, getEndpoint(photos)+"/curated") 187 | } 188 | 189 | // RandomPhotoOpt optional parameters for a random photo search 190 | type RandomPhotoOpt struct { 191 | Height int `url:"h,omitempty"` 192 | Width int `url:"w,omitempty"` 193 | Featured bool `url:"featured,omitempty"` 194 | Username string `url:"username,omitempty"` 195 | SearchQuery string `url:"query,omitempty"` 196 | Count int `url:"count,omitempty"` 197 | Orientation orientation `url:"orientation,omitempty"` 198 | CollectionIDs []int `url:"collections,comma"` 199 | TopicIDs []string `url:"topics,comma"` 200 | } 201 | 202 | // Valid validates a RandomPhotoOpt 203 | func (opt *RandomPhotoOpt) Valid() bool { 204 | //negative values 205 | if opt.Count < 0 || opt.Height < 0 || opt.Width < 0 { 206 | return false 207 | } 208 | //collections/topics and query can't be used at the same time acc to API documentation 209 | if (len(opt.CollectionIDs) != 0 || len(opt.TopicIDs) != 0) && opt.SearchQuery != "" { 210 | return false 211 | } 212 | if opt.Count == 0 { 213 | opt.Count = 1 214 | } 215 | if opt.Orientation != "" { 216 | if opt.Orientation != Landscape && opt.Orientation != Portrait && opt.Orientation != Squarish { 217 | return false 218 | } 219 | } 220 | 221 | return true 222 | } 223 | 224 | // Orientation is orientation of a photo 225 | type orientation string 226 | 227 | // These constants show possible Orientation types 228 | const ( 229 | Landscape orientation = "landscape" 230 | Portrait orientation = "portrait" 231 | Squarish orientation = "squarish" 232 | ) 233 | 234 | var defaultRandomPhotoOpt = &RandomPhotoOpt{Count: 1} 235 | 236 | // Random returns random photo(s). 237 | // If opt is nil, then a single random photo is returned by default 238 | func (ps *PhotosService) Random(opt *RandomPhotoOpt) (*[]Photo, *Response, error) { 239 | if opt == nil { 240 | opt = defaultRandomPhotoOpt 241 | } 242 | if !opt.Valid() { 243 | return nil, nil, &InvalidListOptError{ErrString: "opt provided is not valid."} 244 | } 245 | req, err := newRequest(GET, getEndpoint(photos)+"/random", opt, nil) 246 | if err != nil { 247 | return nil, nil, err 248 | } 249 | resp, err := ps.client.do(req) 250 | if err != nil { 251 | return nil, nil, err 252 | } 253 | photos := make([]Photo, 0) 254 | err = json.Unmarshal(*resp.body, &photos) 255 | if err != nil { 256 | return nil, nil, err 257 | } 258 | return &photos, resp, nil 259 | 260 | } 261 | 262 | // Like likes a photo on the currently authenticated user's behalf 263 | func (ps *PhotosService) Like(photoID string) (*Photo, *Response, error) { 264 | if photoID == "" { 265 | return nil, nil, &IllegalArgumentError{ErrString: "PhotoID cannot be null"} 266 | } 267 | endpoint := fmt.Sprintf("%v/%v/like", getEndpoint(photos), photoID) 268 | req, err := newRequest(POST, endpoint, nil, nil) 269 | if err != nil { 270 | return nil, nil, err 271 | } 272 | resp, err := ps.client.do(req) 273 | if err != nil { 274 | return nil, nil, err 275 | } 276 | var photo Photo 277 | err = json.Unmarshal(*resp.body, &photo) 278 | if err != nil { 279 | return nil, nil, err 280 | } 281 | return &photo, resp, nil 282 | } 283 | 284 | // Unlike likes a photo on the currently authenticated user's behalf 285 | func (ps *PhotosService) Unlike(photoID string) (*Photo, *Response, error) { 286 | if photoID == "" { 287 | return nil, nil, &IllegalArgumentError{ErrString: "PhotoID cannot be null"} 288 | } 289 | endpoint := fmt.Sprintf("%v/%v/like", getEndpoint(photos), photoID) 290 | req, err := newRequest(DELETE, endpoint, nil, nil) 291 | if err != nil { 292 | return nil, nil, err 293 | } 294 | resp, err := ps.client.do(req) 295 | if err != nil { 296 | return nil, nil, err 297 | } 298 | var photo Photo 299 | err = json.Unmarshal(*resp.body, &photo) 300 | if err != nil { 301 | return nil, nil, err 302 | } 303 | return &photo, resp, nil 304 | } 305 | -------------------------------------------------------------------------------- /unsplash/photos_service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "encoding/json" 28 | "io/ioutil" 29 | "log" 30 | "testing" 31 | 32 | "github.com/jarcoal/httpmock" 33 | "github.com/stretchr/testify/assert" 34 | ) 35 | 36 | func TestPhotoOpt(T *testing.T) { 37 | log.SetOutput(ioutil.Discard) 38 | assert := assert.New(T) 39 | var opt, opt2 PhotoOpt 40 | assert.Equal(false, opt.Valid()) 41 | 42 | opt.Height = -42 43 | assert.Equal(false, opt.Valid()) 44 | opt.Crop = true 45 | assert.Equal(false, opt.Valid()) 46 | opt.Height = 0 47 | assert.Equal(false, opt.Valid()) 48 | opt.Height = 100 49 | opt.Width = 100 50 | assert.Equal(true, opt.Valid()) 51 | opt.CropX = 3 52 | opt.CropY = 14 53 | assert.Equal(true, opt.Valid()) 54 | opt.Crop = true 55 | v := processPhotoOpt(&opt) 56 | opt0, ok := v.(rect) 57 | assert.Equal(true, ok) 58 | assert.Equal("3,14,100,100", opt0.Rect) 59 | opt2.Width = 42 60 | opt2.Height = 42 61 | v = processPhotoOpt(&opt2) 62 | _, ok = v.(*PhotoOpt) 63 | assert.Equal(true, ok) 64 | } 65 | 66 | func TestSimplePhoto(T *testing.T) { 67 | log.SetOutput(ioutil.Discard) 68 | assert := assert.New(T) 69 | assert.Nil(nil) 70 | log.SetOutput(ioutil.Discard) 71 | unsplash := setup() 72 | photo, resp, err := unsplash.Photos.Photo("", nil) 73 | assert.NotNil(err) 74 | assert.Nil(photo) 75 | assert.Nil(resp) 76 | 77 | photo, resp, err = unsplash.Photos.Photo("random", nil) 78 | assert.NotNil(photo) 79 | assert.NotNil(resp) 80 | assert.Nil(err) 81 | log.Println(photo) 82 | 83 | json, err := json.Marshal(photo) 84 | assert.Nil(err) 85 | log.Println(string(json)) 86 | } 87 | 88 | func TestPhotoWithOpt(T *testing.T) { 89 | log.SetOutput(ioutil.Discard) 90 | assert := assert.New(T) 91 | var opt PhotoOpt 92 | unsplash := setup() 93 | photo, resp, err := unsplash.Photos.Photo("random", &opt) 94 | assert.NotNil(err) 95 | assert.Nil(resp) 96 | assert.Nil(photo) 97 | log.Println(photo) 98 | opt.Height = 400 99 | opt.Width = 600 100 | photo, resp, err = unsplash.Photos.Photo("random", &opt) 101 | assert.NotNil(photo) 102 | assert.NotNil(resp) 103 | assert.Nil(err) 104 | log.Println(photo) 105 | } 106 | 107 | func TestAllPhotos(T *testing.T) { 108 | assert := assert.New(T) 109 | log.SetOutput(ioutil.Discard) 110 | unsplash := setup() 111 | photos, resp, err := unsplash.Photos.All(nil) 112 | assert.Nil(err) 113 | //check pagination 114 | assert.NotNil(resp) 115 | log.Println(resp) 116 | assert.Equal(true, resp.HasNextPage) 117 | assert.Equal(2, resp.NextPage) 118 | lastPage := resp.LastPage 119 | //check photos 120 | assert.NotNil(photos) 121 | assert.Equal(10, len(*photos)) 122 | 123 | opt := *defaultListOpt 124 | opt.Page = 2 125 | photos, resp, err = unsplash.Photos.All(&opt) 126 | assert.Nil(err) 127 | log.Println(err) 128 | assert.NotNil(resp) 129 | log.Println(resp) 130 | assert.Equal(true, resp.HasNextPage) 131 | assert.Equal(3, resp.NextPage) 132 | assert.Equal(1, resp.PrevPage) 133 | assert.Equal(lastPage, resp.LastPage) 134 | assert.NotNil(photos) 135 | assert.Equal(10, len(*photos)) 136 | 137 | photos, resp, err = unsplash.Photos.All(&ListOpt{PerPage: -1}) 138 | assert.Nil(photos) 139 | assert.Nil(resp) 140 | assert.NotNil(err) 141 | _, ok := err.(*InvalidListOptError) 142 | assert.Equal(true, ok) 143 | 144 | } 145 | func TestCuratedPhotos(T *testing.T) { 146 | T.Skip() // endpoint is removed 147 | assert := assert.New(T) 148 | //TODO write better tests 149 | log.SetOutput(ioutil.Discard) 150 | unsplash := setup() 151 | _, resp, err := unsplash.Photos.Curated(nil) 152 | assert.Nil(err) 153 | //check pagination 154 | assert.NotNil(resp) 155 | log.Println(resp) 156 | } 157 | 158 | func TestPhotoStats(T *testing.T) { 159 | T.Skip() 160 | assert := assert.New(T) 161 | log.SetOutput(ioutil.Discard) 162 | unsplash := setup() 163 | stats, resp, err := unsplash.Photos.Stats("-HPhkZcJQNk") 164 | assert.Nil(err) 165 | assert.NotNil(stats) 166 | assert.NotNil(resp) 167 | log.Println(stats) 168 | 169 | stats, resp, err = unsplash.Photos.Stats("") 170 | assert.Nil(stats) 171 | assert.Nil(resp) 172 | assert.NotNil(err) 173 | } 174 | 175 | func TestPhotoStatistics(T *testing.T) { 176 | assert := assert.New(T) 177 | log.SetOutput(ioutil.Discard) 178 | unsplash := setup() 179 | stats, resp, err := unsplash.Photos.Statistics("-HPhkZcJQNk", nil) 180 | assert.Nil(err) 181 | assert.NotNil(stats) 182 | assert.NotNil(resp) 183 | assert.NotNil(30, stats.Downloads.Historical.Quantity) 184 | log.Println(stats) 185 | 186 | stats, resp, err = unsplash.Photos.Statistics("-HPhkZcJQNk", &StatsOpt{Quantity: 10}) 187 | assert.Nil(err) 188 | assert.NotNil(stats) 189 | assert.NotNil(resp) 190 | assert.NotNil(30, stats.Downloads.Historical.Quantity) 191 | log.Println(stats) 192 | 193 | stats, resp, err = unsplash.Photos.Statistics("-HPhkZcJQNk", &StatsOpt{Resolution: "sd"}) 194 | assert.NotNil(err) 195 | assert.Nil(resp) 196 | 197 | stats, resp, err = unsplash.Photos.Statistics("-HPhkZcJQNk", &StatsOpt{Quantity: 31}) 198 | assert.NotNil(err) 199 | assert.Nil(resp) 200 | 201 | stats, resp, err = unsplash.Photos.Statistics("", nil) 202 | assert.Nil(stats) 203 | assert.Nil(resp) 204 | assert.NotNil(err) 205 | } 206 | 207 | func TestDownloadLink(T *testing.T) { 208 | assert := assert.New(T) 209 | log.SetOutput(ioutil.Discard) 210 | unsplash := setup() 211 | url, resp, err := unsplash.Photos.DownloadLink("-HPhkZcJQNk") 212 | assert.Nil(err) 213 | assert.NotNil(url) 214 | assert.NotNil(resp) 215 | log.Println(url) 216 | 217 | url, resp, err = unsplash.Photos.DownloadLink("") 218 | assert.Nil(url) 219 | assert.Nil(resp) 220 | assert.NotNil(err) 221 | } 222 | 223 | func TestRandomPhotoOpt(T *testing.T) { 224 | assert := assert.New(T) 225 | var opt RandomPhotoOpt 226 | opt.CollectionIDs = []int{42} 227 | opt.SearchQuery = "Gopher" 228 | assert.Equal(false, opt.Valid()) 229 | 230 | var opt2 RandomPhotoOpt 231 | assert.Equal(true, opt2.Valid()) 232 | 233 | opt2.Orientation = "gopher" 234 | assert.Equal(false, opt2.Valid()) 235 | } 236 | func TestRandomPhoto(T *testing.T) { 237 | assert := assert.New(T) 238 | log.SetOutput(ioutil.Discard) 239 | unsplash := setup() 240 | photos, resp, err := unsplash.Photos.Random(nil) 241 | assert.Nil(err) 242 | assert.NotNil(photos) 243 | assert.NotNil(resp) 244 | assert.Equal(1, len(*photos)) 245 | photo := (*photos)[0] 246 | log.Println(photo.String()) 247 | user := photo.Photographer 248 | log.Println(user.String()) 249 | var opt RandomPhotoOpt 250 | opt.Count = 3 251 | opt.SearchQuery = "Earth" 252 | opt.Orientation = Landscape 253 | photos, resp, err = unsplash.Photos.Random(&opt) 254 | assert.Nil(err) 255 | assert.NotNil(photos) 256 | assert.NotNil(resp) 257 | assert.Equal(3, len(*photos)) 258 | 259 | var opt2 RandomPhotoOpt 260 | opt2.Count = 3 261 | opt2.CollectionIDs = []int{151842, 203782} 262 | photos, resp, err = unsplash.Photos.Random(&opt2) 263 | assert.Nil(err) 264 | assert.NotNil(photos) 265 | assert.NotNil(resp) 266 | assert.Equal(3, len(*photos)) 267 | 268 | opt.Count = -1 269 | photos, resp, err = unsplash.Photos.Random(&opt) 270 | assert.NotNil(err) 271 | assert.Nil(photos) 272 | assert.Nil(resp) 273 | //log.Println(photos) 274 | 275 | } 276 | 277 | func TestPhotoLike(T *testing.T) { 278 | T.Skip() 279 | assert := assert.New(T) 280 | log.SetOutput(ioutil.Discard) 281 | unsplash := setup() 282 | photos, resp, err := unsplash.Photos.Random(nil) 283 | assert.Nil(err) 284 | assert.NotNil(photos) 285 | assert.NotNil(resp) 286 | assert.Equal(1, len(*photos)) 287 | photoid := (*photos)[0].ID 288 | photo, resp, err := unsplash.Photos.Like(*photoid) 289 | assert.Nil(err) 290 | assert.NotNil(photo) 291 | assert.NotNil(resp) 292 | 293 | photo, resp, err = unsplash.Photos.Like("") 294 | assert.NotNil(err) 295 | assert.Nil(photo) 296 | assert.Nil(resp) 297 | } 298 | 299 | func TestPhotoUnlike(T *testing.T) { 300 | T.Skip() 301 | assert := assert.New(T) 302 | log.SetOutput(ioutil.Discard) 303 | unsplash := setup() 304 | photos, resp, err := unsplash.Photos.Random(nil) 305 | assert.Nil(err) 306 | assert.NotNil(photos) 307 | assert.NotNil(resp) 308 | assert.Equal(1, len(*photos)) 309 | photoid := (*photos)[0].ID 310 | photo, resp, err := unsplash.Photos.Like(*photoid) 311 | assert.Nil(err) 312 | assert.NotNil(photo) 313 | log.Println(photo.String()) 314 | assert.NotNil(resp) 315 | 316 | photo2, resp, err := unsplash.Photos.Unlike(*photoid) 317 | assert.Nil(err) 318 | assert.NotNil(photo2) 319 | assert.NotNil(resp) 320 | assert.Equal(photo.ID, photo2.ID) 321 | assert.Equal(photo.Color, photo2.Color) 322 | 323 | photo, resp, err = unsplash.Photos.Like("") 324 | assert.NotNil(err) 325 | assert.Nil(photo) 326 | assert.Nil(resp) 327 | } 328 | 329 | func roguePhotoServiceTest(T *testing.T, responder httpmock.Responder) { 330 | httpmock.Activate() 331 | defer httpmock.DeactivateAndReset() 332 | log.SetOutput(ioutil.Discard) 333 | 334 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(photos)+"/gopherPhoto", 335 | responder) 336 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(photos)+"/gopherPhoto/stats", 337 | responder) 338 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(photos)+"/gopherPhoto/download", 339 | responder) 340 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(photos)+"/random?count=1", 341 | responder) 342 | httpmock.RegisterResponder("POST", getEndpoint(base)+getEndpoint(photos)+"/gopherPhoto/like", 343 | responder) 344 | httpmock.RegisterResponder("DELETE", getEndpoint(base)+getEndpoint(photos)+"/gopherPhoto/like", 345 | responder) 346 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(photos), 347 | responder) 348 | 349 | unsplash := setup() 350 | assert := assert.New(T) 351 | photo, resp, err := unsplash.Photos.Photo("gopherPhoto", nil) 352 | assert.Nil(photo) 353 | assert.Nil(resp) 354 | assert.NotNil(err) 355 | log.Println(err) 356 | 357 | photoStats, resp, err := unsplash.Photos.Stats("gopherPhoto") 358 | assert.Nil(photoStats) 359 | assert.Nil(resp) 360 | assert.NotNil(err) 361 | log.Println(err) 362 | 363 | url, resp, err := unsplash.Photos.DownloadLink("gopherPhoto") 364 | assert.Nil(url) 365 | assert.Nil(resp) 366 | assert.NotNil(err) 367 | log.Println(err) 368 | 369 | photos, resp, err := unsplash.Photos.Random(nil) 370 | assert.Nil(photos) 371 | assert.Nil(resp) 372 | assert.NotNil(err) 373 | log.Println(err) 374 | 375 | photo, resp, err = unsplash.Photos.Like("gopherPhoto") 376 | assert.Nil(photo) 377 | assert.Nil(resp) 378 | assert.NotNil(err) 379 | log.Println(err) 380 | 381 | photo, resp, err = unsplash.Photos.Unlike("gopherPhoto") 382 | assert.Nil(photo) 383 | assert.Nil(resp) 384 | assert.NotNil(err) 385 | log.Println(err) 386 | 387 | photos, resp, err = unsplash.Photos.All(nil) 388 | assert.Nil(photos) 389 | assert.Nil(resp) 390 | assert.NotNil(err) 391 | log.Println(err) 392 | } 393 | 394 | func TestPhotoServiceRogueStuff(T *testing.T) { 395 | roguePhotoServiceTest(T, httpmock.NewStringResponder(200, `Bad ass Bug flow`)) 396 | roguePhotoServiceTest(T, nil) 397 | } 398 | -------------------------------------------------------------------------------- /unsplash/collections_service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Hardik Bagdi 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package unsplash 25 | 26 | import ( 27 | "io/ioutil" 28 | "log" 29 | "math/rand" 30 | "strconv" 31 | "testing" 32 | 33 | "github.com/jarcoal/httpmock" 34 | "github.com/stretchr/testify/assert" 35 | ) 36 | 37 | func TestAllCollections(T *testing.T) { 38 | T.Skip() 39 | assert := assert.New(T) 40 | log.SetOutput(ioutil.Discard) 41 | unsplash := setup() 42 | collections, resp, err := unsplash.Collections.All(nil) 43 | assert.Nil(err) 44 | //check pagination 45 | assert.NotNil(resp) 46 | log.Println(resp) 47 | assert.Equal(true, resp.HasNextPage) 48 | assert.Equal(2, resp.NextPage) 49 | lastPage := resp.LastPage 50 | //check collections 51 | assert.NotNil(collections) 52 | assert.Equal(10, len(*collections)) 53 | 54 | opt := *defaultListOpt 55 | opt.Page = 2 56 | collections, resp, err = unsplash.Collections.All(&opt) 57 | assert.Nil(err) 58 | log.Println(err) 59 | assert.NotNil(resp) 60 | log.Println(resp) 61 | assert.Equal(true, resp.HasNextPage) 62 | assert.Equal(3, resp.NextPage) 63 | assert.Equal(1, resp.PrevPage) 64 | assert.Equal(lastPage, resp.LastPage) 65 | assert.NotNil(collections) 66 | assert.Equal(10, len(*collections)) 67 | 68 | collections, resp, err = unsplash.Collections.All(&ListOpt{PerPage: -1}) 69 | assert.Nil(collections) 70 | assert.Nil(resp) 71 | assert.NotNil(err) 72 | _, ok := err.(*InvalidListOptError) 73 | assert.Equal(true, ok) 74 | 75 | } 76 | 77 | func TestFeaturedCollections(T *testing.T) { 78 | T.Skip() 79 | assert := assert.New(T) 80 | log.SetOutput(ioutil.Discard) 81 | unsplash := setup() 82 | collections, resp, err := unsplash.Collections.Featured(nil) 83 | assert.Nil(err) 84 | //check pagination 85 | assert.NotNil(resp) 86 | log.Println(resp) 87 | assert.Equal(true, resp.HasNextPage) 88 | assert.Equal(2, resp.NextPage) 89 | assert.NotNil(collections) 90 | assert.Equal(10, len(*collections)) 91 | } 92 | 93 | func TestCuratedCollections(T *testing.T) { 94 | // Curated collections have been removed from the API 95 | T.Skip() 96 | assert := assert.New(T) 97 | log.SetOutput(ioutil.Discard) 98 | unsplash := setup() 99 | collections, resp, err := unsplash.Collections.Curated(nil) 100 | assert.Nil(err) 101 | //check pagination 102 | assert.NotNil(resp) 103 | log.Println(resp) 104 | assert.Equal(true, resp.HasNextPage) 105 | assert.Equal(2, resp.NextPage) 106 | assert.NotNil(collections) 107 | assert.Equal(10, len(*collections)) 108 | } 109 | 110 | func TestRelatedCollections(T *testing.T) { 111 | T.Skip() 112 | assert := assert.New(T) 113 | log.SetOutput(ioutil.Discard) 114 | unsplash := setup() 115 | collections, resp, err := unsplash.Collections.Related("910", nil) 116 | assert.Nil(err) 117 | //check pagination 118 | assert.NotNil(resp) 119 | assert.NotNil(collections) 120 | log.Println(resp) 121 | 122 | collections, resp, err = unsplash.Collections.Related("", nil) 123 | assert.NotNil(err) 124 | assert.Nil(collections) 125 | assert.Nil(resp) 126 | } 127 | 128 | func TestSimpleCollection(T *testing.T) { 129 | T.Skip() 130 | assert := assert.New(T) 131 | log.SetOutput(ioutil.Discard) 132 | unsplash := setup() 133 | collection, resp, err := unsplash.Collections.Collection("910") 134 | assert.Nil(err) 135 | assert.NotNil(resp) 136 | assert.NotNil(collection) 137 | log.Println(resp) 138 | log.Println(collection) 139 | 140 | collection, resp, err = unsplash.Collections.Collection("") 141 | assert.NotNil(err) 142 | assert.Nil(collection) 143 | assert.Nil(resp) 144 | } 145 | 146 | func TestCreateCollection(T *testing.T) { 147 | T.Skip() 148 | assert := assert.New(T) 149 | log.SetOutput(ioutil.Discard) 150 | unsplash := setup() 151 | var opt CollectionOpt 152 | title := "Test42" 153 | opt.Title = &title 154 | collection, resp, err := unsplash.Collections.Create(&opt) 155 | assert.Nil(err) 156 | assert.NotNil(resp) 157 | assert.NotNil(collection) 158 | resp, err = unsplash.Collections.Delete(*collection.ID) 159 | assert.NotNil(resp) 160 | assert.Nil(err) 161 | 162 | title = "" 163 | collection, resp, err = unsplash.Collections.Create(&opt) 164 | assert.Nil(resp) 165 | assert.Nil(collection) 166 | assert.NotNil(err) 167 | 168 | collection, resp, err = unsplash.Collections.Create(nil) 169 | assert.Nil(resp) 170 | assert.Nil(collection) 171 | assert.NotNil(err) 172 | } 173 | 174 | func TestUpdateCollection(T *testing.T) { 175 | T.Skip() 176 | assert := assert.New(T) 177 | log.SetOutput(ioutil.Discard) 178 | unsplash := setup() 179 | 180 | //get a user's collection 181 | collections, resp, err := unsplash.Users.Collections("gopher", nil) 182 | assert.Nil(err) 183 | assert.NotNil(resp) 184 | assert.NotNil(collections) 185 | collection := (*collections)[0] 186 | assert.NotNil(collection) 187 | log.Println(*collection.ID) 188 | //random title 189 | var opt CollectionOpt 190 | title := "Test43" + strconv.Itoa(rand.Int()) 191 | opt.Title = &title 192 | col, resp, err := unsplash.Collections.Update(*collection.ID, &opt) 193 | assert.Nil(err) 194 | assert.NotNil(resp) 195 | assert.NotNil(col) 196 | 197 | col, resp, err = unsplash.Collections.Update(0, &opt) 198 | assert.Nil(resp) 199 | assert.Nil(col) 200 | assert.NotNil(err) 201 | 202 | col, resp, err = unsplash.Collections.Update(246, nil) 203 | assert.Nil(resp) 204 | assert.Nil(col) 205 | assert.NotNil(err) 206 | 207 | col, resp, err = unsplash.Collections.Update(0, nil) 208 | assert.Nil(resp) 209 | assert.Nil(col) 210 | assert.NotNil(err) 211 | } 212 | 213 | func TestDeleteCollection(T *testing.T) { 214 | T.Skip() 215 | assert := assert.New(T) 216 | log.SetOutput(ioutil.Discard) 217 | unsplash := setup() 218 | var opt CollectionOpt 219 | title := "Test42" 220 | opt.Title = &title 221 | collection, resp, err := unsplash.Collections.Create(&opt) 222 | assert.Nil(err) 223 | assert.NotNil(resp) 224 | assert.NotNil(collection) 225 | 226 | resp, err = unsplash.Collections.Delete(*collection.ID) 227 | assert.NotNil(resp) 228 | assert.Nil(err) 229 | 230 | resp, err = unsplash.Collections.Delete(0) 231 | assert.NotNil(err) 232 | assert.Nil(resp) 233 | } 234 | 235 | func TestAddPhoto(T *testing.T) { 236 | T.Skip() 237 | assert := assert.New(T) 238 | log.SetOutput(ioutil.Discard) 239 | unsplash := setup() 240 | 241 | //get a random photo 242 | photos, resp, err := unsplash.Photos.Random(nil) 243 | assert.Nil(err) 244 | assert.NotNil(resp) 245 | assert.NotNil(photos) 246 | assert.Equal(1, len(*photos)) 247 | photo := (*photos)[0] 248 | assert.NotNil(photo) 249 | 250 | //get a user's collection 251 | collections, resp, err := unsplash.Users.Collections("gopher", nil) 252 | assert.Nil(err) 253 | assert.NotNil(resp) 254 | assert.NotNil(collections) 255 | 256 | collection := (*collections)[0] 257 | assert.NotNil(collection) 258 | 259 | //add the photo 260 | resp, err = unsplash.Collections.AddPhoto(*collection.ID, *photo.ID) 261 | assert.Nil(err) 262 | assert.NotNil(resp) 263 | 264 | //empty things 265 | resp, err = unsplash.Collections.AddPhoto(0, "photoID") 266 | assert.NotNil(err) 267 | assert.Nil(resp) 268 | resp, err = unsplash.Collections.AddPhoto(910, "") 269 | assert.NotNil(err) 270 | assert.Nil(resp) 271 | resp, err = unsplash.Collections.AddPhoto(0, "") 272 | assert.NotNil(err) 273 | assert.Nil(resp) 274 | 275 | } 276 | 277 | func TestRemovePhoto(T *testing.T) { 278 | T.Skip() 279 | assert := assert.New(T) 280 | log.SetOutput(ioutil.Discard) 281 | unsplash := setup() 282 | 283 | //get a random photo 284 | photos, resp, err := unsplash.Photos.Random(nil) 285 | assert.Nil(err) 286 | assert.NotNil(resp) 287 | assert.NotNil(photos) 288 | assert.Equal(1, len(*photos)) 289 | photo := (*photos)[0] 290 | assert.NotNil(photo) 291 | 292 | //get a user's collection 293 | collections, resp, err := unsplash.Users.Collections("gopher", nil) 294 | assert.Nil(err) 295 | assert.NotNil(resp) 296 | assert.NotNil(collections) 297 | 298 | collection := (*collections)[0] 299 | assert.NotNil(collection) 300 | 301 | //add the photo 302 | resp, err = unsplash.Collections.AddPhoto(*collection.ID, *photo.ID) 303 | assert.Nil(err) 304 | assert.NotNil(resp) 305 | 306 | //remove the photo 307 | _, _ = unsplash.Collections.RemovePhoto(*collection.ID, *photo.ID) 308 | // API is being unreliable at the moment. Returns 403 sometimes 309 | // could be because of back-to-back requests? 310 | // assert.Nil(err) 311 | // assert.NotNil(resp) 312 | 313 | //empty stuff 314 | //empty things 315 | resp, err = unsplash.Collections.RemovePhoto(0, "photoID") 316 | assert.NotNil(err) 317 | assert.Nil(resp) 318 | resp, err = unsplash.Collections.RemovePhoto(910, "") 319 | assert.NotNil(err) 320 | assert.Nil(resp) 321 | resp, err = unsplash.Collections.RemovePhoto(0, "") 322 | assert.NotNil(err) 323 | assert.Nil(resp) 324 | } 325 | 326 | func rogueCollectionServiceTest(T *testing.T, responder httpmock.Responder) { 327 | httpmock.Activate() 328 | defer httpmock.DeactivateAndReset() 329 | log.SetOutput(ioutil.Discard) 330 | 331 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(collections)+"/gopherCollection", 332 | responder) 333 | httpmock.RegisterResponder("POST", getEndpoint(base)+getEndpoint(collections)+"?title=gopherCollection", 334 | responder) 335 | httpmock.RegisterResponder("PUT", getEndpoint(base)+getEndpoint(collections)+"/4242?title=gopherCollection", 336 | responder) 337 | httpmock.RegisterResponder("POST", getEndpoint(base)+getEndpoint(collections)+"/4242/add?photo_id=gopherPhoto", 338 | responder) 339 | httpmock.RegisterResponder("DELETE", getEndpoint(base)+getEndpoint(collections)+"/4242/remove?photo_id=gopherPhoto", 340 | responder) 341 | httpmock.RegisterResponder("DELETE", getEndpoint(base)+getEndpoint(collections)+"/4242", 342 | responder) 343 | httpmock.RegisterResponder("GET", getEndpoint(base)+getEndpoint(collections), 344 | responder) 345 | 346 | unsplash := setup() 347 | assert := assert.New(T) 348 | collection, resp, err := unsplash.Collections.Collection("gopherCollection") 349 | assert.Nil(collection) 350 | assert.Nil(resp) 351 | assert.NotNil(err) 352 | log.Println(err) 353 | 354 | var opt CollectionOpt 355 | title := "gopherCollection" 356 | opt.Title = &title 357 | collection, resp, err = unsplash.Collections.Create(&opt) 358 | assert.Nil(collection) 359 | assert.Nil(resp) 360 | assert.NotNil(err) 361 | log.Println(err) 362 | 363 | collection, resp, err = unsplash.Collections.Update(4242, &opt) 364 | assert.Nil(collection) 365 | assert.Nil(resp) 366 | assert.NotNil(err) 367 | log.Println(err) 368 | 369 | resp, err = unsplash.Collections.Delete(4242) 370 | assert.Nil(resp) 371 | assert.NotNil(err) 372 | log.Println(err) 373 | 374 | resp, err = unsplash.Collections.AddPhoto(4242, "gopherPhoto") 375 | assert.NotNil(err) 376 | assert.Nil(resp) 377 | log.Println(err) 378 | 379 | cols, resp, err := unsplash.Collections.All(nil) 380 | assert.Nil(cols) 381 | assert.Nil(resp) 382 | assert.NotNil(err) 383 | log.Println(err) 384 | } 385 | 386 | func TestCollectionServiceRogueStuff(T *testing.T) { 387 | rogueCollectionServiceTest(T, httpmock.NewStringResponder(200, `Bad ass Bug flow`)) 388 | rogueCollectionServiceTest(T, nil) 389 | } 390 | 391 | func TestRemovePhotoRogue(T *testing.T) { 392 | httpmock.Activate() 393 | defer httpmock.DeactivateAndReset() 394 | log.SetOutput(ioutil.Discard) 395 | 396 | httpmock.RegisterResponder("DELETE", getEndpoint(base)+getEndpoint(collections)+"/4242/remove?photo_id=gopherPhoto", 397 | httpmock.NewStringResponder(202, `Bad ass Bug flow`)) 398 | 399 | unsplash := setup() 400 | assert := assert.New(T) 401 | resp, err := unsplash.Collections.RemovePhoto(4242, "gopherPhoto") 402 | assert.NotNil(err) 403 | assert.Nil(resp) 404 | log.Println(err) 405 | 406 | httpmock.RegisterResponder("DELETE", getEndpoint(base)+getEndpoint(collections)+"/4242/remove?photo_id=gopherPhoto", 407 | nil) 408 | 409 | resp, err = unsplash.Collections.RemovePhoto(4242, "gopherPhoto") 410 | assert.Nil(resp) 411 | assert.NotNil(err) 412 | log.Println(err) 413 | } 414 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unsplash API client 2 | 3 | [![GoDoc](https://godoc.org/github.com/hardikbagdi/go-unsplash?status.svg)](https://godoc.org/github.com/hbagdi/go-unsplash/unsplash) 4 | [![CI Test](https://github.com/hbagdi/go-unsplash/actions/workflows/test.yaml/badge.svg)](https://github.com/hbagdi/go-unsplash/actions/workflows/test.yaml) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/hbagdi/go-unsplash)](https://goreportcard.com/report/github.com/hbagdi/go-unsplash) 6 | 7 | A wrapper for the [Unsplash API](https://unsplash.com/developers). 8 | 9 | [Unsplash](https://unsplash.com) provides [freely licensed](https://unsplash.com/license) high-resolution photos that can be used for anything. 10 | 11 | ## Documentation 12 | 13 | - [Installation](#installation) 14 | - [Dependencies](#dependencies) 15 | - [API terms and guidelines](#api-guidelines) 16 | - [Registering your App](#registration) 17 | - [Contact for help](#help) 18 | - [Usage](#usage) 19 | - [License](#license) 20 | 21 | ## Installation 22 | 23 | ```go 24 | go get github.com/hbagdi/go-unsplash/unsplash 25 | ``` 26 | 27 | ## Dependencies 28 | 29 | This library has a single dependency on Google's [go-querystring](https://github.com/google/go-querystring). 30 | 31 | ## API Guidelines 32 | 33 | When using the Unsplash API, you need to make sure to abide by their [API guidelines](https://medium.com/unsplash/unsplash-api-guidelines-28e0216e6daa) and [API Terms](https://unsplash.com/api-terms). 34 | 35 | ## Registration 36 | 37 | [Sign up](https://unsplash.com/join) on Unsplash.com and register as a [developer](https://unsplash.com/developers).
38 | You can then [create a new application](https://unsplash.com/oauth/applications/new) and use the AppID and Secret for authentication. 39 | 40 | ## Help 41 | 42 | Please open an issue in this repository if you need help or want to report a bug.
43 | Mail at the e-mail address in the license if needed. 44 | 45 | ## Usage 46 | 47 | - [Importing](#importing) 48 | - [Authentication](#authentication) 49 | - [Creating an instance](#creating-an-instance) 50 | - [Error handling](#error-handling) 51 | - [Response struct](#response-struct) 52 | - [Pagination](#pagination) - paging through results 53 | - [unsplash.Photos](#photos) 54 | 55 | - [Random](#random) - get random photo(s) 56 | - [All](#all-photos) - get all photos on unplash.com 57 | - [Curated](#curated-photos) - returns a list of curated photos 58 | - [Photo](#photo) -get details of a single photo 59 | - [Like](#like) - like a photo on the authenticated users' behalf 60 | - [Unlike](#unlike) - unlike a photo on the authenticated users' behalf 61 | - [Download link](#download-link) - get download link of a photo 62 | - [Stats](#stats) - statistics of a photo 63 | 64 | - [unsplash.Collections](#collections) 65 | 66 | - [All](#all-collections) - return all collections on unsplash.com 67 | - [Collection](#collection) - get details about a single existing collection 68 | - [Curated](#curated-collections) - get a list of curated collections 69 | - [Featured](#featured-collections) - get a list of featured collections 70 | - [Related](#related-collections) - get a list of related collections of a particular collection 71 | - [Create](#create-collection) - create a collection for the authenticated user 72 | - [Delete](#delete-collection) - delete a collection 73 | - [Update](#update-collection) - update a collection's description 74 | - [Add Photo](#add-photo) - add a photo to a collection 75 | - [Remove Photo](#remove-photo) - remove a photo from a collection 76 | 77 | - [unsplash.Users](#users) 78 | 79 | - [User](#user) - get details about a single user 80 | - [Portfolio](#portfolio) - get link to a user's portfolio 81 | - [Liked Photos](#liked-photos) - get list of photos a user has liked 82 | - [Photos](#user-photos) - get list of photos a user has uploaded to unsplash.com 83 | - [Collections](#user-collections) - collections of a user 84 | 85 | - [unsplash.Search](#search) 86 | 87 | - [Photos](#search-photos) - search photos 88 | - [Collections](#search-collections) - search collections 89 | - [Users](#search-users) - search users 90 | 91 | ### Importing 92 | 93 | Once you've installed the library using [`go get`](#installation), import it to your go project: 94 | 95 | ```go 96 | import "github.com/hbagdi/go-unsplash/unsplash" 97 | ``` 98 | 99 | ### Authentication 100 | 101 | Authentication is not handled by directly by go-unsplash.
102 | Instead, pass an [`http.Client`](https://godoc.org/net/http#Client) that can handle authentication for you.
103 | You can use libraries such as [oauth2](https://godoc.org/golang.org/x/oauth2).
104 | Please note that all calls will include the OAuth token and hence, [`http.Client`](https://godoc.org/net/http#Client) should not be shared between users. 105 | 106 | Note that if you're just using actions that require the public permission scope, only the AppID is required. 107 | 108 | ### Creating an instance 109 | 110 | An instance of unsplash can be created using `New()`.
111 | The http.Client supplied will be used to make requests to the API. 112 | 113 | ```go 114 | ts := oauth2.StaticTokenSource( 115 | // note Client-ID in front of the access token 116 | &oauth2.Token{AccessToken: "Client-ID Your-access-token"}, 117 | ) 118 | client := oauth2.NewClient(oauth2.NoContext, ts) 119 | //use the http.Client to instantiate unsplash 120 | unsplash := unsplash.New(client) 121 | // requests can be now made to the API 122 | randomPhoto, _ , err := unsplash.RandomPhoto(nil) 123 | ``` 124 | 125 | ### Error handling 126 | 127 | All API calls return an `error` as second or third return object. All successful calls will return nil in place of this return. Further, go-unsplash has errors defined as types for better error handling. 128 | 129 | ```go 130 | randomPhoto, _ , err := unsplash.RandomPhoto(nil) 131 | if err != nil { 132 | //handle error 133 | } 134 | ``` 135 | 136 | ### Response struct 137 | 138 | Most API methods return a [`*Response`](https://godoc.org/github.com/hbagdi/go-unsplash/unsplash#Response) along-with the result of the call.
139 | This struct contains paging and rate-limit information. 140 | 141 | ### Pagination 142 | 143 | Pagination is currently supported by supplying a page number in the [`ListOpt`](https://godoc.org/github.com/hbagdi/go-unsplash/unsplash#ListOpt). The `NextPage` field in Response can be used to get the next page number. 144 | 145 | ```go 146 | searchOpt := &SearchOpt{Query : "Batman"} 147 | photos, resp, err := unsplash.Search.Photos(searchOpt) 148 | 149 | if err != nil { 150 | return 151 | } 152 | // process photos 153 | for _,photo := range *photos { 154 | fmt.Println(*photo.ID) 155 | } 156 | // get next 157 | if !resp.HasNextPage { 158 | return 159 | } 160 | searchOpt.Page = resp.NextPage 161 | photos, resp ,err = unsplash.Search.Photos(searchOpt) 162 | //photos now has next page of the search result 163 | ``` 164 | 165 | ### Photos 166 | 167 | Unsplash.Photos is of type PhotosService.
168 | It provides various methods for querying the /photos endpoint of the API. 169 | 170 | #### Random 171 | 172 | You can get a single random photo or multiple depending upon opt. If opt is nil, then a single random photo is returned. Random photos satisfy all the parameters specified in `*RandomPhotoOpt`. 173 | 174 | ```go 175 | photos, resp, err := unsplash.Photos.Random(nil) 176 | assert.Nil(err) 177 | assert.NotNil(photos) 178 | assert.NotNil(resp) 179 | assert.Equal(1, len(*photos)) 180 | var opt RandomPhotoOpt 181 | opt.Count = 3 182 | photos, resp, err = unsplash.Photos.Random(&opt) 183 | assert.Nil(err) 184 | assert.NotNil(photos) 185 | assert.NotNil(resp) 186 | assert.Equal(3, len(*photos)) 187 | ``` 188 | 189 | #### All photos 190 | 191 | Get all photos on unsplash.com.
192 | Obviously, this is a huge list and hence can be paginated through. 193 | 194 | ```go 195 | opt := new(unsplash.ListOpt) 196 | opt.Page = 1 197 | opt.PerPage = 10 198 | 199 | if !opt.Valid() { 200 | fmt.Println("error with opt") 201 | return 202 | } 203 | count := 0 204 | for { 205 | photos, resp, err := un.Photos.All(opt) 206 | 207 | if err != nil { 208 | fmt.Println("error") 209 | return 210 | } 211 | //process photos 212 | for _, c := range *photos { 213 | fmt.Printf("%d : %d\n", count, *c.ID) 214 | count += 1 215 | } 216 | //go for next page 217 | if !resp.HasNextPage { 218 | return 219 | } 220 | opt.Page = resp.NextPage 221 | } 222 | ``` 223 | 224 | #### Curated Photos 225 | 226 | Get all curated photos on unsplash.com.
227 | Obviously, this is a huge list and hence can be paginated through. 228 | 229 | ```go 230 | opt := new(unsplash.ListOpt) 231 | opt.Page = 1 232 | opt.PerPage = 10 233 | 234 | if !opt.Valid() { 235 | fmt.Println("error with opt") 236 | return 237 | } 238 | count := 0 239 | for { 240 | photos, resp, err := un.Photos.Curated(opt) 241 | 242 | if err != nil { 243 | fmt.Println("error") 244 | return 245 | } 246 | //process photos 247 | for _, c := range *photos { 248 | fmt.Printf("%d : %d\n", count, *c.ID) 249 | count += 1 250 | } 251 | //go for next page 252 | if !resp.HasNextPage { 253 | return 254 | } 255 | opt.Page = resp.NextPage 256 | } 257 | ``` 258 | 259 | #### Photo 260 | 261 | Get details of a specific photo. 262 | 263 | ```go 264 | photo, resp, err := unsplash.Photos.Photo("9BoqXzEeQqM", nil) 265 | assert.NotNil(photo) 266 | assert.NotNil(resp) 267 | assert.Nil(err) 268 | fmt.Println(photo) 269 | //photo is of type *Unsplash.Photo 270 | 271 | // you can also specify a PhotoOpt to get a custom size or cropped photo 272 | var opt PhotoOpt 273 | opt.Height = 400 274 | opt.Width = 600 275 | photo, resp, err = unsplash.Photos.Photo("9BoqXzEeQqM", &opt) 276 | assert.NotNil(photo) 277 | assert.NotNil(resp) 278 | assert.Nil(err) 279 | log.Println(photo) 280 | //photo.Urls.Custom will have the cropped photo 281 | // See PhotoOpt for more details 282 | ``` 283 | 284 | #### Like 285 | 286 | ```go 287 | //Like a random photo 288 | photos, resp, err := unsplash.Photos.Random(nil) 289 | assert.Nil(err) 290 | assert.NotNil(photos) 291 | assert.NotNil(resp) 292 | assert.Equal(1, len(*photos)) 293 | photoid := (*photos)[0].ID 294 | photo, resp, err := unsplash.Photos.Like(*photoid) 295 | assert.Nil(err) 296 | assert.NotNil(photo) 297 | assert.NotNil(resp) 298 | ``` 299 | 300 | #### Unlike 301 | 302 | Same way as Like except call `Unlike()`. 303 | 304 | #### Download Link 305 | 306 | Get download URL for a photo. 307 | 308 | ```go 309 | url, resp, err := unsplash.Photos.DownloadLink("-HPhkZcJQNk") 310 | assert.Nil(err) 311 | assert.NotNil(url) 312 | assert.NotNil(resp) 313 | log.Println(url) 314 | ``` 315 | 316 | #### Stats 317 | 318 | Statistics for a specific photo 319 | 320 | ```go 321 | stats, resp, err := unsplash.Photos.Stats("-HPhkZcJQNk") 322 | assert.Nil(err) 323 | assert.NotNil(stats) 324 | assert.NotNil(resp) 325 | log.Println(stats) 326 | ``` 327 | 328 | ### Collections 329 | 330 | Various details about collection(s). 331 | 332 | #### All collections 333 | 334 | ```go 335 | collections, resp, err = unsplash.Collections.All(nil) 336 | assert.Nil(err) 337 | assert.NotNil(resp) 338 | assert.NotNil(collections) 339 | opt := new(ListOpt) 340 | opt.Page = 2 341 | opt.PerPage = 10 342 | //get the second page 343 | collections, resp, err = unsplash.Collections.All(opt) 344 | assert.Nil(err) 345 | assert.NotNil(resp) 346 | assert.NotNil(collections) 347 | ``` 348 | 349 | #### Curated collections 350 | 351 | ```go 352 | collections, resp, err := unsplash.Collections.Curated(nil) 353 | assert.Nil(err) 354 | assert.NotNil(resp) 355 | assert.NotNil(collections) 356 | ``` 357 | 358 | #### Featured collections 359 | 360 | Same as Curated, but use `Featured()` instead. 361 | 362 | #### Related collections 363 | 364 | Get collections related to a collection. 365 | 366 | ```go 367 | collections, resp, err := unsplash.Collections.Related("296", nil) 368 | assert.Nil(err) 369 | assert.NotNil(resp) 370 | assert.NotNil(collections) 371 | //page through if necessary 372 | ``` 373 | 374 | #### Collection 375 | 376 | Details about a specific collection 377 | 378 | ```go 379 | collection, resp, err := unsplash.Collections.Collection("910") 380 | assert.Nil(err) 381 | assert.NotNil(resp) 382 | assert.NotNil(collection) 383 | ``` 384 | 385 | #### Create collection 386 | 387 | Create a collection on behalf of the authenticated user. 388 | 389 | ```go 390 | var opt CollectionOpt 391 | title := "Test42" 392 | opt.Title = &title 393 | collection, resp, err := unsplash.Collections.Create(&opt) 394 | assert.Nil(err) 395 | assert.NotNil(resp) 396 | assert.NotNil(collection) 397 | ``` 398 | 399 | #### Delete collection 400 | 401 | Let's delete the collection just created above. 402 | 403 | ```go 404 | //get list of collections of a user 405 | collections, resp, err := unsplash.Users.Collections("gopher", nil) 406 | assert.Nil(err) 407 | assert.NotNil(resp) 408 | assert.NotNil(collections) 409 | //take the first one 410 | collection := (*collections)[0] 411 | assert.NotNil(collection) 412 | //delete it 413 | resp, err = unsplash.Collections.Delete(*collection.ID) 414 | assert.NotNil(resp) 415 | assert.Nil(err) 416 | ``` 417 | 418 | #### Update collection 419 | 420 | ```go 421 | //get a user's collection 422 | collections, resp, err := unsplash.Users.Collections("gopher", nil) 423 | assert.Nil(err) 424 | assert.NotNil(resp) 425 | assert.NotNil(collections) 426 | // take the first one 427 | collection := (*collections)[0] 428 | assert.NotNil(collection) 429 | log.Println(*collection.ID) 430 | //random title 431 | var opt CollectionOpt 432 | title := "Test43" + strconv.Itoa(rand.Int()) 433 | opt.Title = &title 434 | //update the title 435 | col, resp, err := unsplash.Collections.Update(*collection.ID, &opt) 436 | assert.Nil(err) 437 | assert.NotNil(resp) 438 | assert.NotNil(col) 439 | ``` 440 | 441 | #### Add photo 442 | 443 | ```go 444 | photos, resp, err := unsplash.Photos.Random(nil) 445 | photo := (*photos)[0] 446 | //get a user's collection 447 | collections, resp, err := unsplash.Users.Collections("gopher", nil) 448 | assert.Nil(err) 449 | collection := (*collections)[0] 450 | //add the photo 451 | resp, err = unsplash.Collections.AddPhoto(*collection.ID, *photo.ID) 452 | assert.Nil(err) 453 | ``` 454 | 455 | #### Remove photo 456 | 457 | ```go 458 | //remove a photo 459 | _, _ = unsplash.Collections.RemovePhoto(*collection.ID, *photo.ID) 460 | ``` 461 | 462 | ### Users 463 | 464 | Details about an unsplash.com users. 465 | 466 | #### User 467 | 468 | Details about unsplash.com users. 469 | 470 | ```go 471 | profileImageOpt := &ProfileImageOpt{Height: 120, Width: 400} 472 | //or pass a nil as second arg 473 | user, err := unsplash.Users.User("lukechesser", profileImageOpt) 474 | assert.Nil(err) 475 | assert.NotNil(user) 476 | 477 | //OR, get the currently authenticated user 478 | user, resp, err := unsplash.CurrentUser() 479 | assert.Nil(user) 480 | assert.Nil(resp) 481 | assert.NotNil(err) 482 | ``` 483 | 484 | #### Portfolio 485 | 486 | ```go 487 | url, err = unsplash.Users.Portfolio("gopher") 488 | assert.Nil(err) 489 | assert.NotNil(url) 490 | assert.Equal(url.String(), "https://wikipedia.org/wiki/Gopher") 491 | ``` 492 | 493 | #### Liked Photos 494 | 495 | ```go 496 | photos, resp, err := unsplash.Users.LikedPhotos("lukechesser", nil) 497 | assert.Nil(err) 498 | assert.NotNil(photos) 499 | assert.NotNil(resp) 500 | ``` 501 | 502 | #### User photos 503 | 504 | Get photos a users has uploaded on unsplash.com 505 | 506 | ```go 507 | photos, resp, err := unsplash.Users.Photos("lukechesser", nil) 508 | assert.Nil(err) 509 | assert.NotNil(resp) 510 | assert.NotNil(photos) 511 | ``` 512 | 513 | #### User collections 514 | 515 | Get a list of collections created by the user. 516 | 517 | ```go 518 | collections, resp, err := unsplash.Users.Collections("gopher", nil) 519 | assert.Nil(err) 520 | assert.NotNil(resp) 521 | assert.NotNil(collections) 522 | ``` 523 | 524 | ### Search 525 | 526 | Search for photos, collections or users. 527 | 528 | #### Search photos 529 | 530 | ```go 531 | var opt SearchOpt 532 | //an empty search will be erroneous 533 | photos, resp, err := unsplash.Search.Photos(&opt) 534 | assert.NotNil(err) 535 | assert.Nil(resp) 536 | assert.Nil(photos) 537 | opt.Query = "Nature" 538 | //Search for photos tageed "Nature" 539 | photos, _, err = unsplash.Search.Photos(&opt) 540 | log.Println(len(*photos.Results)) 541 | assert.NotNil(photos) 542 | assert.Nil(err) 543 | ``` 544 | 545 | #### Search collections 546 | 547 | ```go 548 | var opt SearchOpt 549 | opt.Query = "Nature" 550 | collections, _, err = unsplash.Search.Collections(&opt) 551 | assert.NotNil(collections) 552 | assert.Nil(err) 553 | log.Println(len(*collections.Results)) 554 | ``` 555 | 556 | #### Search users 557 | 558 | ```go 559 | var opt SearchOpt 560 | opt.Query = "Nature" 561 | users, _, err = unsplash.Search.Users(&opt) 562 | log.Println(len(*users.Results)) 563 | assert.NotNil(users) 564 | assert.Nil(err) 565 | ``` 566 | 567 | ## License 568 | 569 | Copyright (c) 2017 Hardik Bagdi [hbagdi1@binghamton.edu](mailto:hbagdi1@binghamton.edu) 570 | 571 | MIT License 572 | 573 | 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: 574 | 575 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 576 | 577 | 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. 578 | --------------------------------------------------------------------------------