diff --git a/.gitignore b/.gitignore index 2559738..9d083ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,16 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a @@ -19,7 +32,7 @@ _cgo_export.* _testmain.go -*.exe -*.test *.prof credential.txt + +.idea diff --git a/.gometalinter.json b/.gometalinter.json new file mode 100644 index 0000000..cd32833 --- /dev/null +++ b/.gometalinter.json @@ -0,0 +1,13 @@ +{ + "Disable": [ + "aligncheck", + "gocyclo", + "maligned" + ], + "Deadline": "5m", + "Sort": [ + "path", + "linter" + ], + "Vendor": true +} diff --git a/README.md b/README.md index f9478d2..8f1a26e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ -# go-wp-api -Golang client library for WP-API (Wordpress REST API) +# go-wordpress +[![GoDoc](https://godoc.org/github.com/robbiet480/go-wordpress?status.svg)](https://godoc.org/github.com/robbiet480/go-wordpress) + +A Go client library for the [Wordpress REST API](https://developer.wordpress.org/rest-api/) ## Installation ```bash -go get github.com/sogko/go-wordpress - +go get github.com/robbiet480/go-wordpress ``` ## Usage @@ -16,76 +17,157 @@ go get github.com/sogko/go-wordpress package main import ( - "github.com/sogko/go-wordpress" - "net/http" + "context" + "fmt" + "net/http" + + "github.com/robbiet480/go-wordpress" ) func main() { + tp := wordpress.BasicAuthTransport{ + Username: USER, + Password: PASSWORD, + } + // create wp-api client - client := wordpress.NewClient(&wordpress.Options{ - BaseAPIURL: API_BASE_URL, // example: `http://192.168.99.100:32777/wp-json/wp/v2` - Username: USER, - Password: PASSWORD, - }) - + client, _ := wordpress.NewClient(API_BASE_URL, tp.Client()) + + ctx := context.Background() + // for eg, to get current user (GET /users/me) - currentUser, resp, body, _ := client.Users().Me() - if resp.StatusCode != http.StatusOK { + currentUser, resp, _ := client.Users.Me(ctx, nil) + if resp != nil && resp.StatusCode != http.StatusOK { // handle error } - - // `body` will contain raw JSON body in []bytes - + // Or you can use your own structs (for custom endpoints, for example) - // Below is the equivalent of `client.Posts().Get(100, nil)` + // Below is the equivalent of `client.Posts.Get(ctx, 100, nil)` var obj MyCustomPostStruct - resp, body, err := client.Get("/posts/100", nil, &obj) + resp, err := client.Get(ctx, "/posts/100", nil, &obj) // ... - - log.Println("Current user", currentUser) -} + fmt.Printf("Current user %+v", currentUser) +} ``` + For more examples, see package tests. For list of supported/implemented endpoints, see [Endpoints.md](./endpoints.md) +### Authentication + +The go-wordpress library does not directly handle authentication. Instead, when +creating a new client, pass an `http.Client` that can handle authentication for +you. + +Note that when using an authenticated Client, all calls made by the client will +include the specified authentication transport token. Therefore, authenticated clients should +almost never be shared between different users. + +#### Username/Password or Application Password + +A basic authentication (username/password) client for use with +the [WP-API BasicAuth plugin](https://github.com/WP-API/Basic-Auth) +or [Application Passwords plugin](https://wordpress.org/plugins/application-passwords/) +is included with the library. +An example implementation can be found in [example/basicauth/main.go](example/basicauth/main.go). + +#### OAuth 1.0a + +If you use the [OAuth 1.0a Server](https://github.com/WP-API/OAuth1) for authentication, +you can find an example implementation in [example/oauth2/main.go](example/oauth2/main.go) using the oauth1 library +(which is very similar to the official OAuth 2.0 library). +See the [oauth1 docs](https://godoc.org/dghubble/oauth1) for complete instructions on using that library. + +#### OAuth 2.0 and JWT + +If you are using the [JWT](https://wordpress.org/plugins/jwt-authentication-for-wp-rest-api/) plug-in for authentication, +you can use the [oauth2](https://github.com/golang/oauth2) library's `StaticTokenSource`. +An example implementation can be found in [example/oauth2/main.go](example/oauth2/main.go). +See the [oauth2 docs](https://godoc.org/golang.org/x/oauth2) for complete instructions on using that library. + +#### Other authentication styles + +For any other authentication methods, you should only need to provide a custom `http.Client` when creating a new WordPress client. + +### Pagination + +All requests for resource collections (posts, pages, media, revisions, etc.) +support pagination. Pagination options are described in the +`wordpress.ListOptions` struct and passed to the list methods directly or as an +embedded type of a more specific list options struct (for example +`wordpress.PostListOptions`). Pages information is available via the +`wordpress.Response` struct. + +```go +package main + +import ( + "context" + + "github.com/robbiet480/go-wordpress" +) + +func main() { + tp := wordpress.BasicAuthTransport{ + Username: USER, + Password: PASSWORD, + } + + // create wp-api client + client, _ := wordpress.NewClient(API_BASE_URL, tp.Client()) + + ctx := context.Background() + + opt := &wordpress.PostListOptions{ + ListOptions: wordpress.ListOptions{PerPage: 10}, + } + // get all pages of results + var allPosts []*wordpress.Post + for { + posts, resp, err := client.Posts.List(ctx, opt) + if err != nil { + return err + } + allPosts = append(allPosts, posts...) + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } +} +``` ## Test + __Note:__ Before running the tests, ensure that you have set up your test environment - ### Prerequisites - Wordpress 4.x -- WP-API plugin -- WP-API's BasicAuth plugin (for authentication) -- [WP REST API Meta Endpoints plugin](https://github.com/WP-API/wp-api-meta-endpoints) (for Meta endpoints) +- [WP-API's BasicAuth plugin (for authentication)](https://github.com/WP-API/Basic-Auth) ### Setting up test environment -- Install prequisits (see above) +- Install prequisites (see above) - Import [./test-data/go-wordpress.wordpress.2015-08-23.xml](./test-data/go-wordpress.wordpress.2015-08-23.xml) to your local test Wordpress installation - Upload at least one media to your Wordpress installation (Admin > Media > Upload) - Edit one (1) most recent Post to create a revision - Edit one (1) most recent Page to create a revision -## Running test - +## Running tests ```bash - # Set test enviroment -export WP_API_URL=http://192.168.99.100:32777/wp-json/wp/v2 +export WP_API_URL=http://192.168.99.100:32777/wp-json/ export WP_USER= export WP_PASSWD= -cd /github.com/sogko/go-wordpress +cd $GOPATH/src/github.com/robbiet480/go-wordpress go test - ``` -## TODO -- [ ] `godoc` documentation, so its easier for library users to map the REST APIs to library calls -- [ ] Test `comments` API endpoint. (Currently, already implemented but not tested due to WP-API issues with creating comments reliably) -- [ ] Support OAuth authentication +## Thanks + +Large parts of this library were inspired if not outright copied from Google's excellent [`go-github`](https://github.com/google/go-github) library. diff --git a/categories.go b/categories.go new file mode 100644 index 0000000..25892ee --- /dev/null +++ b/categories.go @@ -0,0 +1,72 @@ +package wordpress + +import ( + "context" + "fmt" +) + +// Category represents a WordPress post/page category. +type Category struct { + ID int `json:"id"` + Count int `json:"count"` + Description string `json:"description"` + Link string `json:"link"` + Name string `json:"name"` + Slug string `json:"slug"` + Taxonomy string `json:"taxonomy"` + Parent int `json:"parent"` +} + +// CategoriesService provides access to the category related functions in the WordPress REST API. +type CategoriesService service + +// List returns a list of categories. +func (c *CategoriesService) List(ctx context.Context, opts *CategoryListOptions) ([]*Category, *Response, error) { + u, err := addOptions("categories", opts) + if err != nil { + return nil, nil, err + } + + req, err := c.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + categories := []*Category{} + resp, err := c.client.Do(ctx, req, &categories) + if err != nil { + return nil, resp, err + } + return categories, resp, nil +} + +// Create creates a new category. +func (c *CategoriesService) Create(ctx context.Context, newCategory *Category) (*Category, *Response, error) { + var created Category + resp, err := c.client.Create(ctx, "categories", newCategory, &created) + return &created, resp, err +} + +// Get returns a single category for the given id. +func (c *CategoriesService) Get(ctx context.Context, id int, params interface{}) (*Category, *Response, error) { + var entity Category + entityURL := fmt.Sprintf("categories/%v", id) + resp, err := c.client.Get(ctx, entityURL, params, &entity) + return &entity, resp, err +} + +// Update updates a single category with the given id. +func (c *CategoriesService) Update(ctx context.Context, id int, post *Category) (*Category, *Response, error) { + var updated Category + entityURL := fmt.Sprintf("categories/%v", id) + resp, err := c.client.Update(ctx, entityURL, post, &updated) + return &updated, resp, err +} + +// Delete removes the category with the given id. +func (c *CategoriesService) Delete(ctx context.Context, id int, params interface{}) (*Category, *Response, error) { + var deleted Category + entityURL := fmt.Sprintf("categories/%v", id) + resp, err := c.client.Delete(ctx, entityURL, params, &deleted) + return &deleted, resp, err +} diff --git a/client.go b/client.go index e1163ad..9fcc0d8 100644 --- a/client.go +++ b/client.go @@ -1,241 +1,524 @@ +// Package wordpress provides a Go client library for the WordPress REST API. package wordpress import ( "bytes" + "context" + "encoding/json" + "errors" "fmt" - "github.com/parnurzeal/gorequest" + "io" "io/ioutil" - "log" + "mime/multipart" "net/http" + "net/url" "reflect" + "strconv" + "strings" + "time" + + "github.com/google/go-querystring/query" ) const ( - CollectionUsers = "users" - CollectionPosts = "posts" - CollectionPages = "pages" - CollectionMedia = "media" - CollectionMeta = "meta" - CollectionRevisions = "revisions" - CollectionComments = "comments" - CollectionTaxonomies = "taxonomies" - CollectionTerms = "terms" - CollectionStatuses = "statuses" - CollectionTypes = "types" + userAgent = "go-wordpress" + headerTotalRecords = "X-WP-Total" + headerTotalPages = "X-WP-TotalPages" ) -type GeneralError struct { - Code string `json:"code"` - Message string `json:"message"` - Data int `json:"data"` // Unsure if this is consistent +// ErrURLContainsWPV2 is returned from NewClient if URL contains /wp/v2. +var ErrURLContainsWPV2 = errors.New("url must not contain /wp/v2") + +// DefaultHTTPTransport is an http.RoundTripper that has DisableKeepAlives set true. +var DefaultHTTPTransport = &http.Transport{ + DisableKeepAlives: true, } -type Options struct { - BaseAPIURL string +// DefaultHTTPClient is an http.Client with the DefaultHTTPTransport and (Cookie) Jar set nil. +var DefaultHTTPClient = &http.Client{ + Jar: nil, + Transport: DefaultHTTPTransport, +} + +// Error is a generic WordPress error container. +type Error struct { + Response *http.Response // HTTP response that caused this error + Code string `json:"code"` + Message string `json:"message"` + Data struct { + Status int `json:"status"` + Params map[string]string `json:"params"` + } `json:"data"` +} - // Basic Auth - Username string - Password string - // TODO: support OAuth authentication +func (e *Error) Error() string { + return fmt.Sprintf("%v %v: %d %v", + e.Response.Request.Method, sanitizeURL(e.Response.Request.URL), + e.Response.StatusCode, e.Message) } +// Client is a struct containing values and methods used for interacting with the WordPress API. type Client struct { - req *gorequest.SuperAgent - options *Options - baseURL string + // User agent used when communicating with the WordPress API. + UserAgent string + + // WordPress timezone location + Location *time.Location + + Categories *CategoriesService + Comments *CommentsService + Media *MediaService + Pages *PagesService + Posts *PostsService + Settings *SettingsService + Statuses *StatusesService + Tags *TagsService + Taxonomies *TaxonomiesService + Terms *TermsService + Types *TypesService + Users *UsersService + + client *http.Client + baseURL *url.URL + + common service // Reuse a single struct instead of allocating one for each service on the heap. } -// Used to create a new SuperAgent object. -func newHTTPClient() *gorequest.SuperAgent { - client := gorequest.New() - client.Client = &http.Client{Jar: nil} - client.Transport = &http.Transport{ - DisableKeepAlives: true, - } - return client +type service struct { + client *Client } -func NewClient(options *Options) *Client { - req := newHTTPClient().SetBasicAuth(options.Username, options.Password) - req = req.RedirectPolicy(func(r gorequest.Request, via []gorequest.Request) error { - // perform BasicAuth on each redirect request. - // (requests are cookie-less; so we need to keep re-auth-ing again) - httpReq := http.Request(*r) - httpReq.SetBasicAuth(options.Username, options.Password) - log.Println("REDIRECT", r, options.Username, options.Password) - return nil - }) - return &Client{ - req: req, - options: options, - baseURL: options.BaseAPIURL, - } +// ListOptions specifies the optional parameters to various List methods that +// support pagination. +type ListOptions struct { + Context string `url:"context,omitempty"` // Scope under which the request is made; determines fields present in response. + Exclude []int `url:"exclude,omitempty,brackets"` // Ensure result set excludes specific IDs. + Include []int `url:"include,omitempty,brackets"` // Limit result set to specific IDs. + Offset int `url:"offset,omitempty"` // Offset the result set by a specific number of items. + Order string `url:"order,omitempty"` // Order sort attribute ascending or descending. + OrderBy string `url:"orderby,omitempty"` // Sort collection by object attribute. + Page int `url:"page,omitempty"` // Current page of the collection. + PerPage int `url:"per_page,omitempty"` // Maximum number of items to be returned in result set. + Search string `url:"search,omitempty"` // Limit results to those matching a string. } -func (client *Client) Users() *UsersCollection { - return &UsersCollection{ - client: client, - url: fmt.Sprintf("%v/%v", client.baseURL, CollectionUsers), - } +// Response is a WordPress REST API response. This wraps the standard http.Response +// returned from WordPress and provides convenient access to things like +// pagination data. +type Response struct { + *http.Response + + // These fields provide the page values for paginating through a set of + // results. Any or all of these may be set to the zero value for + // responses that are not part of a paginated set, or for which there + // are no additional pages. + + TotalRecords int + TotalPages int + PreviousPage int + NextPage int } -func (client *Client) Posts() *PostsCollection { - return &PostsCollection{ - client: client, - url: fmt.Sprintf("%v/%v", client.baseURL, CollectionPosts), - } + +// DeleteResponse is used when deleting an object. +type DeleteResponse struct { + Deleted bool `json:"deleted"` + Previous json.RawMessage `json:"previous"` } -func (client *Client) Pages() *PagesCollection { - return &PagesCollection{ - client: client, - url: fmt.Sprintf("%v/%v", client.baseURL, CollectionPages), - } + +// newResponse creates a new Response for the provided http.Response. +// r must not be nil. +func newResponse(r *http.Response) *Response { + response := &Response{Response: r} + response.populatePageValues() + return response } -func (client *Client) Media() *MediaCollection { - return &MediaCollection{ - client: client, - url: fmt.Sprintf("%v/%v", client.baseURL, CollectionMedia), + +// populatePageValues parses the HTTP Link response headers and populates the +// various pagination link values in the Response. +func (r *Response) populatePageValues() { + totalRecordsHeader := r.Header.Get(headerTotalRecords) + totalRecords, _ := strconv.Atoi(totalRecordsHeader) + + r.TotalRecords = totalRecords + + totalPagesHeader := r.Header.Get(headerTotalPages) + totalPages, _ := strconv.Atoi(totalPagesHeader) + + r.TotalPages = totalPages + + lastPage, _ := strconv.Atoi(r.Request.URL.Query().Get("page")) + + if totalRecordsHeader != "" && totalPagesHeader != "" && lastPage == 0 { + lastPage = 1 } -} -func (client *Client) Comments() *CommentsCollection { - return &CommentsCollection{ - client: client, - url: fmt.Sprintf("%v/%v", client.baseURL, CollectionComments), + + r.PreviousPage = lastPage + + r.NextPage = lastPage + 1 + + if r.NextPage > r.TotalPages { + r.NextPage = 0 } } -func (client *Client) Taxonomies() *TaxonomiesCollection { - return &TaxonomiesCollection{ - client: client, - url: fmt.Sprintf("%v/%v", client.baseURL, CollectionTaxonomies), + +// NewClient returns an initalized Client for the given baseURL and httpClient. +func NewClient(baseURL string, httpClient *http.Client) (*Client, error) { + if strings.Contains(baseURL, "/wp/v2") { + return nil, ErrURLContainsWPV2 + } + + url, urlErr := url.Parse(baseURL) + if urlErr != nil { + return nil, urlErr + } + + if httpClient == nil { + httpClient = DefaultHTTPClient } + + c := &Client{client: httpClient, UserAgent: userAgent, baseURL: url} + c.common.client = c + c.Categories = (*CategoriesService)(&c.common) + c.Comments = (*CommentsService)(&c.common) + c.Media = (*MediaService)(&c.common) + c.Pages = (*PagesService)(&c.common) + c.Posts = (*PostsService)(&c.common) + c.Settings = (*SettingsService)(&c.common) + c.Statuses = (*StatusesService)(&c.common) + c.Tags = (*TagsService)(&c.common) + c.Taxonomies = (*TaxonomiesService)(&c.common) + c.Terms = (*TermsService)(&c.common) + c.Types = (*TypesService)(&c.common) + c.Users = (*UsersService)(&c.common) + return c, nil } -func (client *Client) Terms() *TermsCollection { - return &TermsCollection{ - client: client, - url: fmt.Sprintf("%v/%v", client.baseURL, CollectionTerms), + +// addOptions adds the parameters in opt as URL query parameters to s. opt +// must be a struct whose fields may contain "url" tags. +func addOptions(s string, opt interface{}) (string, error) { + v := reflect.ValueOf(opt) + if v.Kind() == reflect.Ptr && v.IsNil() { + return s, nil } + + if v.Kind() == reflect.String { + return fmt.Sprintf("%s?%s", s, opt.(string)), nil + } + + u, err := url.Parse(s) + if err != nil { + return s, err + } + + qs, err := query.Values(opt) + if err != nil { + return s, err + } + + u.RawQuery = qs.Encode() + return u.String(), nil } -func (client *Client) Statuses() *StatusesCollection { - return &StatusesCollection{ - client: client, - url: fmt.Sprintf("%v/%v", client.baseURL, CollectionStatuses), + +// NewRequest creates an API request. A relative URL can be provided in urlStr, +// in which case it is resolved relative to the baseURL of the Client. +// Relative URLs should always be specified without a preceding slash. If +// specified, the value pointed to by body is JSON encoded and included as the +// request body. +func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { + if !strings.HasSuffix(c.baseURL.Path, "/") { + return nil, fmt.Errorf("baseURL must have a trailing slash, but %q does not", c.baseURL) + } + if urlStr != "" { + urlStr = fmt.Sprintf("/wp-json/wp/v2/%s", urlStr) + } + u, err := c.baseURL.Parse(urlStr) + if err != nil { + return nil, err + } + + var buf io.ReadWriter + if body != nil { + buf = new(bytes.Buffer) + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + if encErr := enc.Encode(body); encErr != nil { + return nil, encErr + } } + + req, err := http.NewRequest(method, u.String(), buf) + if err != nil { + return nil, err + } + + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + if c.UserAgent != "" { + req.Header.Set("User-Agent", c.UserAgent) + } + return req, nil } -func (client *Client) Types() *TypesCollection { - return &TypesCollection{ - client: client, - url: fmt.Sprintf("%v/%v", client.baseURL, CollectionTypes), + +// Do sends an API request and returns the API response. The API response is +// JSON decoded and stored in the value pointed to by v, or returned as an +// error if an API error has occurred. If v implements the io.Writer +// interface, the raw response body will be written to v, without attempting to +// first decode it. +// +// The provided ctx must be non-nil. If it is canceled or times out, +// ctx.Err() will be returned. +func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) { + req = req.WithContext(ctx) + + resp, err := c.client.Do(req) + if err != nil { + // If we got an error, and the context has been canceled, + // the context's error is probably more useful. + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + // If the error type is *url.Error, sanitize its URL before returning. + if e, ok := err.(*url.Error); ok { + if url, urlErr := url.Parse(e.URL); urlErr == nil { + e.URL = sanitizeURL(url).String() + return nil, e + } + } + + return nil, err + } + + // nolint: errcheck + defer func() { + // Drain up to 512 bytes and close the body to let the Transport reuse the connection + io.CopyN(ioutil.Discard, resp.Body, 512) + resp.Body.Close() + }() + + response := newResponse(resp) + + err = checkResponse(resp) + if err != nil { + // even though there was an error, we still return the response + // in case the caller wants to inspect it further + return response, err + } + + if v != nil { + if w, ok := v.(io.Writer); ok { + if _, copyErr := io.Copy(w, resp.Body); copyErr != nil { + err = copyErr + } + } else { + err = json.NewDecoder(resp.Body).Decode(v) + if err == io.EOF { + err = nil // ignore EOF errors caused by empty response body + } + } } + + return response, err +} + +// RootInfo is a struct containing basic and publicly available information about the WordPress REST API. +type RootInfo struct { + Authentication interface{} `json:"authentication"` + Description string `json:"description"` + GMTOffset int `json:"gmt_offset"` + HomeURL string `json:"home"` + Name string `json:"name"` + Namespaces []string `json:"namespaces"` + PermalinkStructure string `json:"permalink_structure"` + TimezoneString string `json:"timezone_string"` + URL string `json:"url"` + + Location *time.Location `json:"-"` } -func (client *Client) List(url string, params interface{}, result interface{}) (*http.Response, []byte, error) { - client.req.TargetType = "json" - resp, body, errSlice := client.req.Get(url).Query(params).EndBytes() - if errSlice != nil && len(errSlice) > 0 { - return nil, body, errSlice[len(errSlice)-1] +// BasicInfo gets basic and publicly available information about the WordPress REST API. +func (c *Client) BasicInfo(ctx context.Context) (*RootInfo, *Response, error) { + var entity RootInfo + + resp, err := c.Get(ctx, "", nil, &entity) + if err != nil { + return &entity, resp, err + } + + location, locationErr := time.LoadLocation(entity.TimezoneString) + if locationErr != nil { + return &entity, resp, locationErr } - err := unmarshallResponse(resp, body, result) - _resp := http.Response(*resp) - return &_resp, body, err + entity.Location = location + + return &entity, resp, err } -func (client *Client) Create(url string, content interface{}, result interface{}) (*http.Response, []byte, error) { - contentVal := unpackInterfacePointer(content) - client.req.TargetType = "json" - req := client.req.Post(url).Send(contentVal) - resp, body, errSlice := req.EndBytes() - if errSlice != nil && len(errSlice) > 0 { - return nil, body, errSlice[len(errSlice)-1] + +// List is a generic function that will return a list of items from the WordPress REST API. +func (c *Client) List(ctx context.Context, url string, params interface{}, result interface{}) (*Response, error) { + + u, err := addOptions(url, params) + if err != nil { + return nil, err } - err := unmarshallResponse(resp, body, result) - _resp := http.Response(*resp) - return &_resp, body, err + + req, err := c.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + + return c.Do(ctx, req, &result) } -func (client *Client) Get(url string, params interface{}, result interface{}) (*http.Response, []byte, error) { - client.req.TargetType = "json" - resp, body, errSlice := client.req.Get(url).Query(params).EndBytes() - if errSlice != nil && len(errSlice) > 0 { - return nil, body, errSlice[len(errSlice)-1] + +// Create creates a new item on the WordPress REST API. +func (c *Client) Create(ctx context.Context, url string, content interface{}, result interface{}) (*Response, error) { + req, err := c.NewRequest("POST", url, content) + if err != nil { + return nil, err } - err := unmarshallResponse(resp, body, result) - _resp := http.Response(*resp) - return &_resp, body, err + + return c.Do(ctx, req, &result) } -func (client *Client) Update(url string, content interface{}, result interface{}) (*http.Response, []byte, error) { - contentVal := unpackInterfacePointer(content) +// Get returns a single item from the WordPress REST API for the given parameters. +func (c *Client) Get(ctx context.Context, url string, params interface{}, result interface{}) (*Response, error) { + u, err := addOptions(url, params) + if err != nil { + return nil, err + } - client.req.TargetType = "json" - req := client.req.Post(url).Send(contentVal) - req.Set("HTTP_X_HTTP_METHOD_OVERRIDE", "PUT") - resp, body, errSlice := req.EndBytes() - if errSlice != nil && len(errSlice) > 0 { - return nil, body, errSlice[len(errSlice)-1] + req, err := c.NewRequest("GET", u, nil) + if err != nil { + return nil, err } - err := unmarshallResponse(resp, body, result) - _resp := http.Response(*resp) - return &_resp, body, err + + return c.Do(ctx, req, &result) } -func (client *Client) Delete(url string, params interface{}, result interface{}) (*http.Response, []byte, error) { - client.req.TargetType = "json" - req := client.req.Get(url).Query(params).Query("_method=DELETE") - req.Set("HTTP_X_HTTP_METHOD_OVERRIDE", "DELETE") - resp, body, errSlice := req.End() - by := []byte(body) - if errSlice != nil && len(errSlice) > 0 { - return resp, by, errSlice[len(errSlice)-1] + +// Update will update an item on the WordPress REST API. +func (c *Client) Update(ctx context.Context, url string, content interface{}, result interface{}) (*Response, error) { + req, err := c.NewRequest("PUT", url, content) + if err != nil { + return nil, err } - err := unmarshallResponse(resp, by, result) - _resp := http.Response(*resp) - return &_resp, by, err -} -func (client *Client) PostData(url string, content []byte, contentType string, filename string, result interface{}) (*http.Response, []byte, error) { - // gorequest does not support POST-ing raw data - // so, we have to manually create a HTTP client - s := client.req.Post(url) + req.Header.Set("HTTP_X_HTTP_METHOD_OVERRIDE", "PUT") + + return c.Do(ctx, req, &result) +} - buf := bytes.NewBuffer(content) +// Delete will delete an item from the WordPress REST API. +func (c *Client) Delete(ctx context.Context, url string, params interface{}, result interface{}) (*Response, error) { + u, err := addOptions(url, params) + if err != nil { + return nil, err + } - req, err := http.NewRequest(s.Method, s.Url, buf) + req, err := c.NewRequest("DELETE", u, nil) if err != nil { - return nil, nil, err + return nil, err } - req.Header.Set("Content-Type", contentType) - req.Header.Set("Content-Disposition", fmt.Sprintf("filename=%v", filename)) + req.Header.Set("HTTP_X_HTTP_METHOD_OVERRIDE", "DELETE") - // Add basic auth - req.SetBasicAuth(s.BasicAuth.Username, s.BasicAuth.Password) + if req.URL.Query().Get("force") != "" { + var deleteResp DeleteResponse - // Set Transport - s.Client.Transport = s.Transport + resp, err := c.Do(ctx, req, &deleteResp) + if err != nil { + return resp, err + } - // Send request - resp, err := s.Client.Do(req) + if deleteResp.Deleted { + if err := json.Unmarshal(deleteResp.Previous, &result); err != nil { + return resp, err + } + } + + return resp, nil + + } + return c.Do(ctx, req, &result) +} + +// PostData allows uploading of binary objects to the WordPress REST API. +func (c *Client) PostData(ctx context.Context, urlStr string, content []byte, contentType string, filename string, result interface{}) (*Response, error) { + + // gorequest does not support POST-ing raw data + // so, we have to manually create a HTTP client + var buf bytes.Buffer + w := multipart.NewWriter(&buf) + fileField, fileFieldErr := w.CreateFormFile("file", filename) + if fileFieldErr != nil { + return nil, fileFieldErr + } + if _, writeErr := fileField.Write(content); writeErr != nil { + return nil, writeErr + } + if closeErr := w.Close(); closeErr != nil { + return nil, closeErr + } + + if !strings.HasSuffix(c.baseURL.Path, "/") { + return nil, fmt.Errorf("baseURL must have a trailing slash, but %q does not", c.baseURL) + } + if urlStr != "" { + urlStr = fmt.Sprintf("/wp-json/wp/v2/%s", urlStr) + } + u, err := c.baseURL.Parse(urlStr) if err != nil { - return nil, nil, err + return nil, err } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + req, err := http.NewRequest("POST", u.String(), &buf) if err != nil { - return nil, nil, err + return nil, err } - err = unmarshallResponse(resp, body, result) - _resp := http.Response(*resp) - return &_resp, body, err + if c.UserAgent != "" { + req.Header.Set("User-Agent", c.UserAgent) + } + + req.Header.Set("Content-Type", w.FormDataContentType()) + req.Header.Set("Content-Disposition", fmt.Sprintf("filename=%v", filename)) + + // Send request + return c.Do(ctx, req, &result) } -func unpackInterfacePointer(content interface{}) interface{} { - val := reflect.ValueOf(content) - for val.Kind() == reflect.Ptr { - if val.IsNil() { - return nil - } - val = val.Elem() +// sanitizeURL redacts the password parameter from the URL which may be +// exposed to the user. +func sanitizeURL(uri *url.URL) *url.URL { + if uri == nil { + return nil + } + params := uri.Query() + if len(params.Get("password")) > 0 { + params.Set("password", "REDACTED") + uri.RawQuery = params.Encode() + } + return uri +} + +// checkResponse checks the API response for errors, and returns them if +// present. A response is considered an error if it has a status code outside +// the 200 range or equal to 202 Accepted. +// API error responses are expected to have either no response +// body, or a JSON response body that maps to ErrorResponse. Any other +// response body will be silently ignored. +func checkResponse(r *http.Response) error { + if c := r.StatusCode; 200 <= c && c <= 299 { + return nil } - if val.IsValid() { - return val.Interface() + errorResponse := &Error{Response: r} + data, err := ioutil.ReadAll(r.Body) + if err == nil && data != nil { + if jsonErr := json.Unmarshal(data, errorResponse); jsonErr != nil { + return jsonErr + } } - return nil + return errorResponse } diff --git a/client_test.go b/client_test.go index 86af5ae..9947e99 100644 --- a/client_test.go +++ b/client_test.go @@ -1,9 +1,12 @@ package wordpress_test import ( - "github.com/sogko/go-wordpress" + "context" + "errors" "os" "testing" + + "github.com/robbiet480/go-wordpress" ) var USER string = os.Getenv("WP_USER") @@ -11,14 +14,19 @@ var PASSWORD string = os.Getenv("WP_PASSWD") var API_BASE_URL string = os.Getenv("WP_API_URL") func TestClientNew(t *testing.T) { - client := wordpress.NewClient(&wordpress.Options{ - BaseAPIURL: API_BASE_URL, - Username: USER, - Password: PASSWORD, - }) + + tp := wordpress.BasicAuthTransport{ + Username: USER, + Password: PASSWORD, + } + client, clientErr := wordpress.NewClient(API_BASE_URL, tp.Client()) if client == nil { t.Fatalf("Client should not be nil") } + + if clientErr != nil { + t.Fatal("Error parsing URL") + } } /** @@ -26,15 +34,26 @@ Test helper functions */ // initTestClient creates test wordpress client -func initTestClient() *wordpress.Client { +func initTestClient() (*wordpress.Client, context.Context) { if API_BASE_URL == "" { panic("Please set your environment before running the tests") } - return wordpress.NewClient(&wordpress.Options{ - BaseAPIURL: API_BASE_URL, - Username: USER, - Password: PASSWORD, - }) + tp := wordpress.BasicAuthTransport{ + Username: USER, + Password: PASSWORD, + } + + client, clientErr := wordpress.NewClient(API_BASE_URL, tp.Client()) + + if client == nil { + panic(errors.New("client should not be nil")) + } + + if clientErr != nil { + panic(errors.New("error parsing url")) + } + + return client, context.Background() } diff --git a/comments.go b/comments.go index 41050fc..d1ecaac 100644 --- a/comments.go +++ b/comments.go @@ -1,61 +1,82 @@ package wordpress import ( + "context" "fmt" - "net/http" ) +// Comment represents a WordPress post comment. type Comment struct { - ID int `json:"id,omitempty"` - AvatarURL string `json:"avatar_url,omitempty"` - AvatarURLs AvatarURLS `json:"avatar_urls,omitempty"` - Author int `json:"author,omitempty"` - AuthorEmail string `json:"author_email,omitempty"` - AuthorIP string `json:"author_ip,omitempty"` - AuthorName string `json:"author_name,omitempty"` - AuthorURL string `json:"author_url,omitempty"` - AuthorUserAgent string `json:"author_user_agent,omitempty"` - Content Content `json:"content,omitempty"` - Date string `json:"date,omitempty"` - DateGMT string `json:"date_gmt,omitempty"` - Karma int `json:"karma,omitempty"` - Link string `json:"link,omitempty"` - Parent int `json:"parent,omitempty"` - Post int `json:"post,omitempty"` - Status string `json:"status,omitempty"` - Type string `json:"type,omitempty"` + ID int `json:"id,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + AvatarURLs AvatarURLS `json:"avatar_urls,omitempty"` + Author int `json:"author,omitempty"` + AuthorEmail string `json:"author_email,omitempty"` + AuthorIP string `json:"author_ip,omitempty"` + AuthorName string `json:"author_name,omitempty"` + AuthorURL string `json:"author_url,omitempty"` + AuthorUserAgent string `json:"author_user_agent,omitempty"` + Content RenderedString `json:"content,omitempty"` + Date Time `json:"date,omitempty"` + DateGMT Time `json:"date_gmt,omitempty"` + Karma int `json:"karma,omitempty"` + Link string `json:"link,omitempty"` + Parent int `json:"parent,omitempty"` + Post int `json:"post,omitempty"` + Status string `json:"status,omitempty"` + Type string `json:"type,omitempty"` } -type CommentsCollection struct { - client *Client - url string -} +// CommentsService provides access to the comment related functions in the WordPress REST API. +type CommentsService service + +// List returns a list of comments. +func (c *CommentsService) List(ctx context.Context, opts *CommentListOptions) ([]*Comment, *Response, error) { + u, err := addOptions("comments", opts) + if err != nil { + return nil, nil, err + } -func (col *CommentsCollection) List(params interface{}) ([]Comment, *http.Response, []byte, error) { - var comments []Comment - resp, body, err := col.client.List(col.url, params, &comments) - return comments, resp, body, err + req, err := c.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + comments := []*Comment{} + resp, err := c.client.Do(ctx, req, &comments) + if err != nil { + return nil, resp, err + } + return comments, resp, nil } -func (col *CommentsCollection) Create(new *Comment) (*Comment, *http.Response, []byte, error) { + +// Create creates a new comment. +func (c *CommentsService) Create(ctx context.Context, newComment *Comment) (*Comment, *Response, error) { var created Comment - resp, body, err := col.client.Create(col.url, new, &created) - return &created, resp, body, err + resp, err := c.client.Create(ctx, "comments", newComment, &created) + return &created, resp, err } -func (col *CommentsCollection) Get(id int, params interface{}) (*Comment, *http.Response, []byte, error) { + +// Get returns a single comment for the given id. +func (c *CommentsService) Get(ctx context.Context, id int, params interface{}) (*Comment, *Response, error) { var entity Comment - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Get(entityURL, params, &entity) - return &entity, resp, body, err + entityURL := fmt.Sprintf("comments/%v", id) + resp, err := c.client.Get(ctx, entityURL, params, &entity) + return &entity, resp, err } -func (col *CommentsCollection) Update(id int, post *Comment) (*Comment, *http.Response, []byte, error) { + +// Update updates a single comment with the given id. +func (c *CommentsService) Update(ctx context.Context, id int, post *Comment) (*Comment, *Response, error) { var updated Comment - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Update(entityURL, post, &updated) - return &updated, resp, body, err + entityURL := fmt.Sprintf("comments/%v", id) + resp, err := c.client.Update(ctx, entityURL, post, &updated) + return &updated, resp, err } -func (col *CommentsCollection) Delete(id int, params interface{}) (*Comment, *http.Response, []byte, error) { + +// Delete removes the comment with the given id. +func (c *CommentsService) Delete(ctx context.Context, id int, params interface{}) (*Comment, *Response, error) { var deleted Comment - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Delete(entityURL, params, &deleted) - return &deleted, resp, body, err + entityURL := fmt.Sprintf("comments/%v", id) + resp, err := c.client.Delete(ctx, entityURL, params, &deleted) + return &deleted, resp, err } diff --git a/comments_test.go b/comments_test.go index 828e7e8..f1e09a5 100644 --- a/comments_test.go +++ b/comments_test.go @@ -1,10 +1,12 @@ package wordpress_test import ( - "github.com/sogko/go-wordpress" + "context" "log" "net/http" "testing" + + "github.com/robbiet480/go-wordpress" ) func factoryComment(postID int) wordpress.Comment { @@ -13,7 +15,7 @@ func factoryComment(postID int) wordpress.Comment { Author: 1, Status: wordpress.CommentStatusApproved, AuthorName: "go-wordpress", - Content: wordpress.Content{ + Content: wordpress.RenderedString{ Raw: "Test Comment", Rendered: "

Test Comment

", }, @@ -22,15 +24,12 @@ func factoryComment(postID int) wordpress.Comment { func cleanUpComment(t *testing.T, commentID int) { - wp := initTestClient() - deletedComment, resp, body, err := wp.Comments().Delete(commentID, "force=true") + wp, ctx := initTestClient() + deletedComment, resp, err := wp.Comments.Delete(ctx, commentID, "force=true") if err != nil { t.Errorf("Failed to clean up new comment: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if deletedComment.ID != commentID { @@ -38,41 +37,38 @@ func cleanUpComment(t *testing.T, commentID int) { } } -func getAnyOneComment(t *testing.T, wp *wordpress.Client) *wordpress.Comment { +func getAnyOneComment(t *testing.T, ctx context.Context, wp *wordpress.Client) *wordpress.Comment { - comments, resp, body, err := wp.Comments().List(nil) - if resp.StatusCode != http.StatusOK { + comments, resp, err := wp.Comments.List(ctx, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if len(comments) < 1 { log.Print(err) - log.Print(body) log.Print(resp) t.Fatalf("Should not return empty comments") } commentID := comments[0].ID - comment, resp, _, _ := wp.Comments().Get(commentID, "context=edit") - if resp.StatusCode != http.StatusOK { + comment, resp, _ := wp.Comments.Get(ctx, commentID, "context=edit") + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } return comment } func TestCommentsList(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - comments, resp, body, err := wp.Comments().List(nil) + comments, resp, err := wp.Comments.List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if comments == nil { t.Errorf("Should not return nil comments") } @@ -82,60 +78,53 @@ func TestCommentsList(t *testing.T) { } func TestCommentsGet_CommentExists(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - c := getAnyOneComment(t, wp) + c := getAnyOneComment(t, ctx, wp) - comment, resp, body, err := wp.Comments().Get(c.ID, nil) + comment, resp, err := wp.Comments.Get(ctx, c.ID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if comment == nil { t.Errorf("Should not return nil comments") } } func TestCommentsGet_CommentDoesNotExists(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - comment, resp, body, err := wp.Comments().Get(-1, nil) + comment, resp, err := wp.Comments.Get(ctx, -1, nil) if err == nil { t.Errorf("Should return error") } - if resp.StatusCode != http.StatusNotFound { + if resp != nil && resp.StatusCode != http.StatusNotFound { t.Errorf("Expected 404 Not Found, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if comment == nil { t.Errorf("Should not return nil comments") } } func TestCommentsCreate(t *testing.T) { - t.Skipf("[TestCommentsCreate] Skipped: there is an issue with creating comments, server returning empty string") - wp := initTestClient() + wp, ctx := initTestClient() - p := getAnyOnePost(t, wp) + p := getAnyOnePost(t, ctx, wp) c := factoryComment(p.ID) - newComment, resp, body, err := wp.Comments().Create(&c) + newComment, resp, err := wp.Comments.Create(ctx, &c) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusCreated { + if resp != nil && resp.StatusCode != http.StatusCreated { t.Errorf("Expected 201 Created, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if newComment == nil { t.Errorf("Should not return nil newComment") } diff --git a/discovery.go b/discovery.go new file mode 100644 index 0000000..d2105c4 --- /dev/null +++ b/discovery.go @@ -0,0 +1,86 @@ +package wordpress + +import ( + "context" + "net/http" + + "github.com/PuerkitoBio/goquery" + "github.com/tomnomnom/linkheader" +) + +// DiscoveredAPI is a struct containing details about a discovered WordPress REST API. +type DiscoveredAPI struct { + BaseURL string + DiscoveredURL string + ViaHeader bool + ViaHTML bool + Client *Client + BasicInfo *RootInfo +} + +// DiscoverAPI will discover the API root URL for the given base URL. +func DiscoverAPI(baseURL string, getRootInfo bool) (*DiscoveredAPI, error) { + discovered := &DiscoveredAPI{ + BaseURL: baseURL, + } + res, httpErr := http.Get(baseURL) + if httpErr != nil { + return nil, httpErr + } + if res.Header.Get("Link") != "" { + discoveredURL, linkErr := linkHeader(res) + if linkErr != nil { + return nil, linkErr + } + discovered.DiscoveredURL = discoveredURL + discovered.ViaHeader = true + } else { + discoveredURL, linkErr := extractLinkFromHTML(res) + if linkErr != nil { + return nil, linkErr + } + discovered.DiscoveredURL = discoveredURL + discovered.ViaHTML = true + } + if getRootInfo { + client, clientErr := NewClient(discovered.DiscoveredURL, nil) + if clientErr != nil { + return nil, clientErr + } + info, _, basicInfoErr := client.BasicInfo(context.Background()) + if basicInfoErr != nil { + return nil, basicInfoErr + } + client.Location = info.Location + discovered.BasicInfo = info + discovered.Client = client + return discovered, nil + } + client, clientErr := NewClient(discovered.DiscoveredURL, nil) + if clientErr != nil { + return nil, clientErr + } + discovered.Client = client + return discovered, nil +} + +func linkHeader(resp *http.Response) (string, error) { + for _, link := range linkheader.Parse(resp.Header.Get("Link")) { + if link.Rel == "https://api.w.org/" { + return link.URL, nil + } + } + return "", nil +} + +func extractLinkFromHTML(resp *http.Response) (string, error) { + doc, docErr := goquery.NewDocumentFromResponse(resp) + if docErr != nil { + return "", docErr + } + href, hrefExists := doc.Find(`link[rel="https://api.w.org/"]`).Attr("href") + if hrefExists { + return href, nil + } + return "", nil +} diff --git a/endpoints.md b/endpoints.md index 45d8f4d..7a1172c 100644 --- a/endpoints.md +++ b/endpoints.md @@ -18,32 +18,6 @@ List of WP-API REST endpoints and implementation status - [ ] `PUT /comments/[id]` (Implemented but untested) - [ ] `DELETE /comments/[id]` (Implemented but untested) -## Meta - -- [x] `GET /[parent_base]/[parent_id]/meta` -- [x] `POST /[parent_base]/[parent_id]/meta` -- [x] `GET /[parent_base]/[parent_id]/meta/[id]` -- [x] `PUT /[parent_base]/[parent_id]/meta/[id]` -- [x] `DELETE /[parent_base]/[parent_id]/meta/[id]` - -`[parent_base] = "posts" | "pages"` - -### Meta Posts - -- [x] `GET /posts/[post_id]/meta` -- [x] `POST /posts/[post_id]/meta` -- [x] `GET /posts/[post_id]/meta/[id]` -- [x] `PUT /posts/[post_id]/meta/[id]` -- [x] `DELETE /posts/[post_id]/meta/[id]` - -### Meta Pages - -- [x] `GET /pages/[post_id]/meta` -- [x] `POST /pages/[post_id]/meta` -- [x] `GET /pages/[post_id]/meta/[id]` -- [x] `PUT /pages/[post_id]/meta/[id]` -- [x] `DELETE /pages/[post_id]/meta/[id]` - ## Post Statuses - [x] `GET /statuses` @@ -154,4 +128,7 @@ List of WP-API REST endpoints and implementation status - [x] `DELETE /users/[id]` - [x] `GET /users/me` +## Settings +- [x] `GET /settings` +- [x] `POST /settings` diff --git a/example/basicauth/main.go b/example/basicauth/main.go new file mode 100644 index 0000000..e2c7e91 --- /dev/null +++ b/example/basicauth/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "log" + + "github.com/robbiet480/go-wordpress" +) + +func main() { + + tp := wordpress.BasicAuthTransport{ + Username: "username", + Password: "password", + } + + // create wp-api client + client, _ := wordpress.NewClient("http://192.168.99.100:32777/wp-json/", tp.Client()) + + ctx := context.Background() + + // get the currently authenticated users details + authenticatedUser, _, err := client.Users.Me(ctx, nil) + if err != nil { + log.Fatalln(err) + } + log.Printf("Authenticated user %+v", authenticatedUser) +} diff --git a/example/oauth1/main.go b/example/oauth1/main.go new file mode 100644 index 0000000..0d9bc69 --- /dev/null +++ b/example/oauth1/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/dghubble/oauth1" + "github.com/robbiet480/go-wordpress" +) + +var config oauth1.Config + +// main performs the WordPress OAuth1 user flow from the command line +func main() { + config = oauth1.Config{ + ConsumerKey: "CONSUMER_KEY", + ConsumerSecret: "CONSUMER_SECRET", + CallbackURL: "http://localhost:8080/callback", + Endpoint: oauth1.Endpoint{ + RequestTokenURL: "http://192.168.99.100:32777/oauth1/request", + AuthorizeURL: "http://192.168.99.100:32777/oauth1/authorize", + AccessTokenURL: "http://192.168.99.100:32777/oauth1/access", + }, + } + + requestToken, requestSecret, err := login() + if err != nil { + log.Fatalf("Request Token Phase: %s", err.Error()) + } + accessToken, err := receiveVerifier(requestToken, requestSecret) + if err != nil { + log.Fatalf("Access Token Phase: %s", err.Error()) + } + + log.Println("Consumer was granted an access token to act on behalf of a user.") + log.Printf("token: %s\nsecret: %s\n", accessToken.Token, accessToken.TokenSecret) + + ctx := context.Background() + + httpClient := config.Client(ctx, accessToken) + + // create wp-api client + client, _ := wordpress.NewClient("http://192.168.99.100:32777/wp-json/", httpClient) + + // get the currently authenticated users details + authenticatedUser, _, err := client.Users.Me(ctx, nil) + if err != nil { + log.Fatalln(err) + } + log.Printf("Authenticated user %+v", authenticatedUser) +} + +func login() (requestToken, requestSecret string, err error) { + requestToken, requestSecret, err = config.RequestToken() + if err != nil { + return "", "", err + } + authorizationURL, err := config.AuthorizationURL(requestToken) + if err != nil { + return "", "", err + } + fmt.Printf("Open this URL in your browser:\n%s\n", authorizationURL.String()) + return requestToken, requestSecret, err +} + +func receiveVerifier(requestToken, requestSecret string) (*oauth1.Token, error) { + fmt.Printf("Choose whether to grant the application access.\nPaste " + + "the oauth_verifier parameter from the address bar: ") + var verifier string + _, err := fmt.Scanf("%s", &verifier) + accessToken, accessSecret, err := config.AccessToken(requestToken, requestSecret, verifier) + if err != nil { + return nil, err + } + return oauth1.NewToken(accessToken, accessSecret), err +} diff --git a/example/oauth2/main.go b/example/oauth2/main.go new file mode 100644 index 0000000..a69badb --- /dev/null +++ b/example/oauth2/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "context" + "log" + + "github.com/robbiet480/go-wordpress" + "golang.org/x/oauth2" +) + +func main() { + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: "JWT_TOKEN"}, + ) + tc := oauth2.NewClient(ctx, ts) + + client, _ := wordpress.NewClient("http://192.168.99.100:32777/wp-json/", tc) + + // get the currently authenticated users details + authenticatedUser, _, err := client.Users.Me(ctx, nil) + if err != nil { + log.Fatalln(err) + } + log.Printf("Authenticated user %+v", authenticatedUser) +} diff --git a/list_options.go b/list_options.go new file mode 100644 index 0000000..370f062 --- /dev/null +++ b/list_options.go @@ -0,0 +1,99 @@ +//go:generate schema-generate -o list_options.go -p wordpress wp_api_json_schema.json +// must use https://github.com/robbiet480/generate for comments + url tag + brackets +// Code generated by schema-generate. DO NOT EDIT. + +package wordpress + +import "time" + +// CategoryListOptions are options that can be passed to List(). +type CategoryListOptions struct { + HideEmpty bool `url:"hide_empty,omitempty"` // Whether to hide terms not assigned to any posts. + Parent int `url:"parent,omitempty"` // Limit result set to terms assigned to a specific parent. + Post int `url:"post,omitempty"` // Limit result set to terms assigned to a specific post. + Slug []string `url:"slug,omitempty,brackets"` // Limit result set to terms with one or more specific slugs. + + ListOptions +} + +// CommentListOptions are options that can be passed to List(). +type CommentListOptions struct { + After *time.Time `url:"after,omitempty"` // Limit response to comments published after a given ISO8601 compliant date. + Author []int `url:"author,omitempty,brackets"` // Limit result set to comments assigned to specific user IDs. Requires authorization. + AuthorEmail string `url:"author_email,omitempty"` // Limit result set to that from a specific author email. Requires authorization. + AuthorExclude []int `url:"author_exclude,omitempty,brackets"` // Ensure result set excludes comments assigned to specific user IDs. Requires authorization. + Before *time.Time `url:"before,omitempty"` // Limit response to comments published before a given ISO8601 compliant date. + Parent []int `url:"parent,omitempty,brackets"` // Limit result set to comments of specific parent IDs. + ParentExclude []int `url:"parent_exclude,omitempty,brackets"` // Ensure result set excludes specific parent IDs. + Password string `url:"password,omitempty"` // The password for the post if it is password protected. + Post []int `url:"post,omitempty,brackets"` // Limit result set to comments assigned to specific post IDs. + Status string `url:"status,omitempty"` // Limit result set to comments assigned a specific status. Requires authorization. + Type string `url:"type,omitempty"` // Limit result set to comments assigned a specific type. Requires authorization. + + ListOptions +} + +// MediaListOptions are options that can be passed to List(). +type MediaListOptions struct { + After *time.Time `url:"after,omitempty"` // Limit response to posts published after a given ISO8601 compliant date. + Author []int `url:"author,omitempty,brackets"` // Limit result set to posts assigned to specific authors. + AuthorExclude []int `url:"author_exclude,omitempty,brackets"` // Ensure result set excludes posts assigned to specific authors. + Before *time.Time `url:"before,omitempty"` // Limit response to posts published before a given ISO8601 compliant date. + MediaType string `url:"media_type,omitempty"` // Limit result set to attachments of a particular media type. + MimeType string `url:"mime_type,omitempty"` // Limit result set to attachments of a particular MIME type. + Parent []int `url:"parent,omitempty,brackets"` // Limit result set to items with particular parent IDs. + ParentExclude []int `url:"parent_exclude,omitempty,brackets"` // Limit result set to all items except those of a particular parent ID. + Slug []string `url:"slug,omitempty,brackets"` // Limit result set to posts with one or more specific slugs. + Status []string `url:"status,omitempty,brackets"` // Limit result set to posts assigned one or more statuses. + + ListOptions +} + +// PageListOptions are options that can be passed to List(). +type PageListOptions struct { + After *time.Time `url:"after,omitempty"` // Limit response to posts published after a given ISO8601 compliant date. + Author []int `url:"author,omitempty,brackets"` // Limit result set to posts assigned to specific authors. + AuthorExclude []int `url:"author_exclude,omitempty,brackets"` // Ensure result set excludes posts assigned to specific authors. + Before *time.Time `url:"before,omitempty"` // Limit response to posts published before a given ISO8601 compliant date. + MenuOrder int `url:"menu_order,omitempty"` // Limit result set to posts with a specific menu_order value. + Parent []int `url:"parent,omitempty,brackets"` // Limit result set to items with particular parent IDs. + ParentExclude []int `url:"parent_exclude,omitempty,brackets"` // Limit result set to all items except those of a particular parent ID. + Slug []string `url:"slug,omitempty,brackets"` // Limit result set to posts with one or more specific slugs. + Status []string `url:"status,omitempty,brackets"` // Limit result set to posts assigned one or more statuses. + + ListOptions +} + +// PostListOptions are options that can be passed to List(). +type PostListOptions struct { + After *time.Time `url:"after,omitempty"` // Limit response to posts published after a given ISO8601 compliant date. + Author []int `url:"author,omitempty,brackets"` // Limit result set to posts assigned to specific authors. + AuthorExclude []int `url:"author_exclude,omitempty,brackets"` // Ensure result set excludes posts assigned to specific authors. + Before *time.Time `url:"before,omitempty"` // Limit response to posts published before a given ISO8601 compliant date. + Categories []int `url:"categories,omitempty,brackets"` // Limit result set to all items that have the specified term assigned in the categories taxonomy. + CategoriesExclude []int `url:"categories_exclude,omitempty,brackets"` // Limit result set to all items except those that have the specified term assigned in the categories taxonomy. + Slug []string `url:"slug,omitempty,brackets"` // Limit result set to posts with one or more specific slugs. + Status []string `url:"status,omitempty,brackets"` // Limit result set to posts assigned one or more statuses. + Sticky bool `url:"sticky,omitempty"` // Limit result set to items that are sticky. + Tags []int `url:"tags,omitempty,brackets"` // Limit result set to all items that have the specified term assigned in the tags taxonomy. + TagsExclude []int `url:"tags_exclude,omitempty,brackets"` // Limit result set to all items except those that have the specified term assigned in the tags taxonomy. + + ListOptions +} + +// TagListOptions are options that can be passed to List(). +type TagListOptions struct { + HideEmpty bool `url:"hide_empty,omitempty"` // Whether to hide terms not assigned to any posts. + Post int `url:"post,omitempty"` // Limit result set to terms assigned to a specific post. + Slug []string `url:"slug,omitempty,brackets"` // Limit result set to terms with one or more specific slugs. + + ListOptions +} + +// UserListOptions are options that can be passed to List(). +type UserListOptions struct { + Roles []string `url:"roles,omitempty,brackets"` // Limit result set to users matching at least one specific role provided. Accepts csv list or single role. + Slug []string `url:"slug,omitempty,brackets"` // Limit result set to users with one or more specific slugs. + + ListOptions +} diff --git a/media.go b/media.go index 749da48..02aee9a 100644 --- a/media.go +++ b/media.go @@ -1,10 +1,11 @@ package wordpress import ( + "context" "fmt" - "net/http" ) +// MediaDetailsSizesItem provides details for a single media item's size. type MediaDetailsSizesItem struct { File string `json:"file,omitempty"` Width int `json:"width,omitempty"` @@ -12,12 +13,17 @@ type MediaDetailsSizesItem struct { MimeType string `json:"mime_type,omitempty"` SourceURL string `json:"source_url,omitempty"` } + +// MediaDetailsSizes provides different sizes of the same media item. type MediaDetailsSizes struct { Thumbnail MediaDetailsSizesItem `json:"thumbnail,omitempty"` Medium MediaDetailsSizesItem `json:"medium,omitempty"` Large MediaDetailsSizesItem `json:"large,omitempty"` SiteLogo MediaDetailsSizesItem `json:"site-logo,omitempty"` + Full MediaDetailsSizesItem `json:"full,omitempty"` } + +// MediaDetails describes specific details about media. type MediaDetails struct { Raw string `json:"raw,omitempty"` Rendered string `json:"rendered,omitempty"` @@ -27,59 +33,82 @@ type MediaDetails struct { Sizes MediaDetailsSizes `json:"sizes,omitempty"` ImageMeta map[string]interface{} `json:"image_meta,omitempty"` } + +// MediaUploadOptions are options that can be passed to Create(). type MediaUploadOptions struct { Filename string ContentType string Data []byte } + +// Media represents a WordPress post media. type Media struct { - ID int `json:"id,omitempty"` - Date string `json:"date,omitempty"` - DateGMT string `json:"date_gmt,omitempty"` - GUID GUID `json:"guid,omitempty"` - Link string `json:"link,omitempty"` - Modified string `json:"modified,omitempty"` - ModifiedGMT string `json:"modifiedGMT,omitempty"` - Password string `json:"password,omitempty"` - Slug string `json:"slug,omitempty"` - Status string `json:"status,omitempty"` - Type string `json:"type,omitempty"` - Title Title `json:"title,omitempty"` - Author int `json:"author,omitempty"` - MediaStatus string `json:"comment_status,omitempty"` - PingStatus string `json:"ping_status,omitempty"` - AltText string `json:"alt_text,omitempty"` - Caption string `json:"caption,omitempty"` - Description string `json:"description,omitempty"` - MediaType string `json:"media_type,omitempty"` - MediaDetails MediaDetails `json:"media_details,omitempty"` - Post int `json:"post,omitempty"` - SourceURL string `json:"source_url,omitempty"` -} -type MediaCollection struct { - client *Client - url string + ID int `json:"id,omitempty"` + Date Time `json:"date,omitempty"` + DateGMT Time `json:"date_gmt,omitempty"` + GUID RenderedString `json:"guid,omitempty"` + Link string `json:"link,omitempty"` + Modified Time `json:"modified,omitempty"` + ModifiedGMT Time `json:"modifiedGMT,omitempty"` + Password string `json:"password,omitempty"` + Slug string `json:"slug,omitempty"` + Status string `json:"status,omitempty"` + Type string `json:"type,omitempty"` + Title RenderedString `json:"title,omitempty"` + Author int `json:"author,omitempty"` + MediaStatus string `json:"media_status,omitempty"` + PingStatus string `json:"ping_status,omitempty"` + AltText string `json:"alt_text,omitempty"` + Caption RenderedString `json:"caption,omitempty"` + Description RenderedString `json:"description,omitempty"` + MediaType string `json:"media_type,omitempty"` + MediaDetails MediaDetails `json:"media_details,omitempty"` + Post int `json:"post,omitempty"` + SourceURL string `json:"source_url,omitempty"` } -func (col *MediaCollection) List(params interface{}) ([]Media, *http.Response, []byte, error) { - var media []Media - resp, body, err := col.client.List(col.url, params, &media) - return media, resp, body, err +// MediaService provides access to the media related functions in the WordPress REST API. +type MediaService service + +// List returns a list of medias. +func (c *MediaService) List(ctx context.Context, opts *MediaListOptions) ([]*Media, *Response, error) { + u, err := addOptions("media", opts) + if err != nil { + return nil, nil, err + } + + req, err := c.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + media := []*Media{} + resp, err := c.client.Do(ctx, req, &media) + if err != nil { + return nil, resp, err + } + return media, resp, nil } -func (col *MediaCollection) Create(options *MediaUploadOptions) (*Media, *http.Response, []byte, error) { + +// Create creates a new media. +func (c *MediaService) Create(ctx context.Context, options *MediaUploadOptions) (*Media, *Response, error) { var created Media - resp, body, err := col.client.PostData(col.url, options.Data, options.ContentType, options.Filename, &created) - return &created, resp, body, err + resp, err := c.client.PostData(ctx, "media", options.Data, options.ContentType, options.Filename, &created) + return &created, resp, err } -func (col *MediaCollection) Get(id int, params interface{}) (*Media, *http.Response, []byte, error) { + +// Get returns a single media item for the given id. +func (c *MediaService) Get(ctx context.Context, id int, params interface{}) (*Media, *Response, error) { var entity Media - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Get(entityURL, params, &entity) - return &entity, resp, body, err + entityURL := fmt.Sprintf("media/%v", id) + resp, err := c.client.Get(ctx, entityURL, params, &entity) + return &entity, resp, err } -func (col *MediaCollection) Delete(id int, params interface{}) (*Media, *http.Response, []byte, error) { + +// Delete removes the media item with the given id. +func (c *MediaService) Delete(ctx context.Context, id int, params interface{}) (*Media, *Response, error) { var deleted Media - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Delete(entityURL, params, &deleted) - return &deleted, resp, body, err + entityURL := fmt.Sprintf("media/%v", id) + resp, err := c.client.Delete(ctx, entityURL, params, &deleted) + return &deleted, resp, err } diff --git a/media_test.go b/media_test.go index 8d1da34..8c05690 100644 --- a/media_test.go +++ b/media_test.go @@ -1,24 +1,26 @@ package wordpress_test import ( - "github.com/sogko/go-wordpress" + "context" "io/ioutil" "net/http" "os" "testing" + + "github.com/robbiet480/go-wordpress" ) func factoryMediaFileUpload(t *testing.T) *wordpress.MediaUploadOptions { - // assuming current-working directory `{GO_WORKSPACE_PATH}/src/github.com/sogko/go-wordpress` + // assuming current-working directory `{GO_WORKSPACE_PATH}/src/github.com/robbiet480/go-wordpress` path := "./test-data/test-media.jpg" // prepare file to upload file, err := os.Open(path) - defer file.Close() if err != nil { t.Fatalf("Failed to open test media file to upload: %v", err.Error()) } + defer file.Close() fileContents, err := ioutil.ReadAll(file) if err != nil { t.Fatalf("Failed to read test media file to upload: %v", err.Error()) @@ -32,10 +34,11 @@ func factoryMediaFileUpload(t *testing.T) *wordpress.MediaUploadOptions { } return &media } -func getAnyOneMedia(t *testing.T, wp *wordpress.Client) *wordpress.Media { - media, resp, _, _ := wp.Media().List(nil) - if resp.StatusCode != http.StatusOK { +func getAnyOneMedia(t *testing.T, ctx context.Context, wp *wordpress.Client) *wordpress.Media { + + media, resp, _ := wp.Media.List(ctx, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if len(media) < 1 { @@ -44,8 +47,8 @@ func getAnyOneMedia(t *testing.T, wp *wordpress.Client) *wordpress.Media { mediaID := media[0].ID - m, resp, _, _ := wp.Media().Get(mediaID, "context=edit") - if resp.StatusCode != http.StatusOK { + m, resp, _ := wp.Media.Get(ctx, mediaID, "context=edit") + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if m == nil { @@ -54,36 +57,32 @@ func getAnyOneMedia(t *testing.T, wp *wordpress.Client) *wordpress.Media { return m } -func cleanUpMedia(t *testing.T, wp *wordpress.Client, mediaID int) { +func cleanUpMedia(t *testing.T, ctx context.Context, wp *wordpress.Client, mediaID int) { - deletedMedia, resp, body, err := wp.Media().Delete(mediaID, "force=true") + deletedMedia, resp, err := wp.Media.Delete(ctx, mediaID, "force=true") if err != nil { t.Errorf("Failed to clean up new media: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if deletedMedia.ID != mediaID { t.Errorf("Deleted comment ID should be the same as newly created comment: %v != %v", deletedMedia.ID, mediaID) } } + func TestMediaList(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - media, resp, body, err := wp.Media().List(nil) + media, resp, err := wp.Media.List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if media == nil { t.Errorf("Should not return nil media") } @@ -93,20 +92,18 @@ func TestMediaList(t *testing.T) { } func TestMediaGet_Exists(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - m := getAnyOneMedia(t, wp) + m := getAnyOneMedia(t, ctx, wp) - media, resp, body, err := wp.Media().Get(m.ID, nil) + media, resp, err := wp.Media.Get(ctx, m.ID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if media == nil { t.Errorf("Should not return nil media") } @@ -114,18 +111,16 @@ func TestMediaGet_Exists(t *testing.T) { } func TestMediaGet_DoesNotExists(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - media, resp, body, err := wp.Media().Get(-1, nil) + media, resp, err := wp.Media.Get(ctx, -1, nil) if err == nil { t.Errorf("Should return error") } - if resp.StatusCode != http.StatusNotFound { + if resp != nil && resp.StatusCode != http.StatusNotFound { t.Errorf("Expected 404 Not Found, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if media == nil { t.Errorf("Should not return nil media") } @@ -134,22 +129,20 @@ func TestMediaGet_DoesNotExists(t *testing.T) { func TestMediaCreate(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() media := factoryMediaFileUpload(t) - newMedia, resp, body, err := wp.Media().Create(media) + newMedia, resp, err := wp.Media.Create(ctx, media) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusCreated { + if resp != nil && resp.StatusCode != http.StatusCreated { t.Errorf("Expected 201 Created, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if newMedia == nil { t.Errorf("Should not return nil newMedia") } - cleanUpMedia(t, wp, newMedia.ID) + cleanUpMedia(t, ctx, wp, newMedia.ID) } diff --git a/meta.go b/meta.go deleted file mode 100644 index d2700f3..0000000 --- a/meta.go +++ /dev/null @@ -1,54 +0,0 @@ -package wordpress - -import ( - "fmt" - "log" - "net/http" -) - -type Meta struct { - ID int `json:"id,omitempty"` - Key string `json:"key,omitempty"` - Value string `json:"value,omitempty"` -} - -type MetaDeletedResponse struct { - Message string `json:"message,omitempty"` -} - -type MetaCollection struct { - client *Client - url string - parent interface{} - parentType string -} - -func (col *MetaCollection) List(params interface{}) ([]Meta, *http.Response, []byte, error) { - var meta []Meta - resp, body, err := col.client.List(col.url, params, &meta) - return meta, resp, body, err -} -func (col *MetaCollection) Create(new *Meta) (*Meta, *http.Response, []byte, error) { - var created Meta - resp, body, err := col.client.Create(col.url, new, &created) - return &created, resp, body, err -} -func (col *MetaCollection) Get(id int, params interface{}) (*Meta, *http.Response, []byte, error) { - var meta Meta - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Get(entityURL, params, &meta) - return &meta, resp, body, err -} -func (col *MetaCollection) Update(id int, meta *Meta) (*Meta, *http.Response, []byte, error) { - var updated Meta - entityURL := fmt.Sprintf("%v/%v", col.url, id) - log.Println("URL", entityURL) - resp, body, err := col.client.Update(entityURL, meta, &updated) - return &updated, resp, body, err -} -func (col *MetaCollection) Delete(id int, params interface{}) (*MetaDeletedResponse, *http.Response, []byte, error) { - var response MetaDeletedResponse - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Delete(entityURL, params, &response) - return &response, resp, body, err -} diff --git a/pages.go b/pages.go index 8852947..9710180 100644 --- a/pages.go +++ b/pages.go @@ -1,131 +1,143 @@ package wordpress import ( + "context" "fmt" - "net/http" + "log" ) +// Page represents a WordPress page. type Page struct { - collection *PagesCollection `json:"-"` - - ID int `json:"id,omitempty"` - Date string `json:"date,omitempty"` - DateGMT string `json:"date_gmt,omitempty"` - GUID GUID `json:"guid,omitempty"` - Link string `json:"link,omitempty"` - Modified string `json:"modified,omitempty"` - ModifiedGMT string `json:"modifiedGMT,omitempty"` - Password string `json:"password,omitempty"` - Slug string `json:"slug,omitempty"` - Status string `json:"status,omitempty"` - Type string `json:"type,omitempty"` - Parent int `json:"parent,omitempty"` - Title Title `json:"title,omitempty"` - Content Content `json:"content,omitempty"` - Author int `json:"author,omitempty"` - Excerpt Excerpt `json:"excerpt,omitempty"` - FeaturedImage int `json:"featured_image,omitempty"` - CommentStatus string `json:"comment_status,omitempty"` - PingStatus string `json:"ping_status,omitempty"` - MenuOrder int `json:"menu_order,omitempty"` - Template string `json:"template,omitempty"` + collection *PagesService + + ID int `json:"id,omitempty"` + Date Time `json:"date,omitempty"` + DateGMT Time `json:"date_gmt,omitempty"` + GUID RenderedString `json:"guid,omitempty"` + Link string `json:"link,omitempty"` + Modified Time `json:"modified,omitempty"` + ModifiedGMT Time `json:"modifiedGMT,omitempty"` + Password string `json:"password,omitempty"` + Slug string `json:"slug,omitempty"` + Status string `json:"status,omitempty"` + Type string `json:"type,omitempty"` + Parent int `json:"parent,omitempty"` + Title RenderedString `json:"title,omitempty"` + Content RenderedString `json:"content,omitempty"` + Author int `json:"author,omitempty"` + Excerpt RenderedString `json:"excerpt,omitempty"` + FeaturedImage int `json:"featured_image,omitempty"` + CommentStatus string `json:"comment_status,omitempty"` + PingStatus string `json:"ping_status,omitempty"` + MenuOrder int `json:"menu_order,omitempty"` + Template string `json:"template,omitempty"` } -func (entity *Page) setCollection(col *PagesCollection) { - entity.collection = col +func (entity *Page) setService(c *PagesService) { + entity.collection = c } -func (entity *Page) Meta() *MetaCollection { - if entity.collection == nil { - // missing page.collection parent. Probably Page struct was initialized manually. - _warning("Missing parent page collection") - return nil - } - return &MetaCollection{ - client: entity.collection.client, - parent: entity, - parentType: CollectionPages, - url: fmt.Sprintf("%v/%v/%v", entity.collection.url, entity.ID, CollectionMeta), - } -} -func (entity *Page) Revisions() *RevisionsCollection { + +// Revisions gets the revisions of a single page. +func (entity *Page) Revisions() *RevisionsService { if entity.collection == nil { // missing page.collection parent. Probably Page struct was initialized manually, not fetched from API - _warning("Missing parent page collection") + log.Println("[go-wordpress] Missing parent page collection") return nil } - return &RevisionsCollection{ - client: entity.collection.client, + return &RevisionsService{ + service: service(*entity.collection), parent: entity, - parentType: CollectionPages, - url: fmt.Sprintf("%v/%v/%v", entity.collection.url, entity.ID, CollectionRevisions), + parentType: "pages", + url: fmt.Sprintf("%v/%v/%v", "pages", entity.ID, "revisions"), } } -func (entity *Page) Populate(params interface{}) (*Page, *http.Response, []byte, error) { - return entity.collection.Get(entity.ID, params) +// Populate will fill a manually initialized page with the collection information. +func (entity *Page) Populate(ctx context.Context, params interface{}) (*Page, *Response, error) { + return entity.collection.Get(ctx, entity.ID, params) } -type PagesCollection struct { - client *Client - url string - entityURL string -} +// PagesService provides access to the page related functions in the WordPress REST API. +type PagesService service -func (col *PagesCollection) List(params interface{}) ([]Page, *http.Response, []byte, error) { - var pages []Page - resp, body, err := col.client.List(col.url, params, &pages) +// List returns a list of pages. +func (c *PagesService) List(ctx context.Context, opts *PageListOptions) ([]*Page, *Response, error) { + u, err := addOptions("pages", opts) + if err != nil { + return nil, nil, err + } + + req, err := c.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + pages := []*Page{} + resp, err := c.client.Do(ctx, req, &pages) + if err != nil { + return nil, resp, err + } // set collection object for each entity which has sub-collection for _, p := range pages { - p.setCollection(col) + p.setService(c) } - return pages, resp, body, err + return pages, resp, nil } -func (col *PagesCollection) Create(new *Page) (*Page, *http.Response, []byte, error) { + +// Create creates a new page. +func (c *PagesService) Create(ctx context.Context, newPage *Page) (*Page, *Response, error) { var created Page - resp, body, err := col.client.Create(col.url, new, &created) + resp, err := c.client.Create(ctx, "pages", newPage, &created) - created.setCollection(col) + created.setService(c) - return &created, resp, body, err + return &created, resp, err } -func (col *PagesCollection) Get(id int, params interface{}) (*Page, *http.Response, []byte, error) { + +// Get returns a single page for the given id. +func (c *PagesService) Get(ctx context.Context, id int, params interface{}) (*Page, *Response, error) { var entity Page - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Get(entityURL, params, &entity) + entityURL := fmt.Sprintf("pages/%v", id) + resp, err := c.client.Get(ctx, entityURL, params, &entity) // set collection object for each entity which has sub-collection - entity.setCollection(col) + entity.setService(c) - return &entity, resp, body, err + return &entity, resp, err } -func (col *PagesCollection) Entity(id int) *Page { + +// Entity returns a basic page for the given id. +func (c *PagesService) Entity(id int) *Page { entity := Page{ - collection: col, + collection: c, ID: id, } return &entity } -func (col *PagesCollection) Update(id int, page *Page) (*Page, *http.Response, []byte, error) { +// Update updates a single page with the given id. +func (c *PagesService) Update(ctx context.Context, id int, page *Page) (*Page, *Response, error) { var updated Page - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Update(entityURL, page, &updated) + entityURL := fmt.Sprintf("pages/%v", id) + resp, err := c.client.Update(ctx, entityURL, page, &updated) // set collection object for each entity which has sub-collection - updated.setCollection(col) + updated.setService(c) - return &updated, resp, body, err + return &updated, resp, err } -func (col *PagesCollection) Delete(id int, params interface{}) (*Page, *http.Response, []byte, error) { + +// Delete removes the page with the given id. +func (c *PagesService) Delete(ctx context.Context, id int, params interface{}) (*Page, *Response, error) { var deleted Page - entityURL := fmt.Sprintf("%v/%v", col.url, id) + entityURL := fmt.Sprintf("pages/%v", id) - resp, body, err := col.client.Delete(entityURL, params, &deleted) + resp, err := c.client.Delete(ctx, entityURL, params, &deleted) // set collection object for each entity which has sub-collection - deleted.setCollection(col) + deleted.setService(c) - return &deleted, resp, body, err + return &deleted, resp, err } diff --git a/pages_meta_test.go b/pages_meta_test.go deleted file mode 100644 index 4d1043d..0000000 --- a/pages_meta_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package wordpress_test - -import ( - "github.com/sogko/go-wordpress" - "net/http" - "testing" -) - -func cleanUpPageMeta(t *testing.T, page *wordpress.Page, metaId int) { - - // note: Need to pass in `force=true` param in order to delete page meta - deletedMeta, resp, body, err := page.Meta().Delete(metaId, "force=true") - if err != nil { - t.Errorf("Failed to clean up new page: %v", err.Error()) - } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected 200 OK, got %v", resp.Status) - } - if deletedMeta.Message != "Deleted meta" { - t.Errorf("Unexpected response to deleted meta: %v", deletedMeta.Message) - } - -} - -func TestPagesMeta_InvalidCall(t *testing.T) { - // User is not allowed to call create wordpress.Page object manually to retrieve PageMetaCollection - // A proper API call would inject the right PageMetaCollection, Client and other goodies into a page, - // allowing user to call page.Meta() - invalidPage := wordpress.Page{} - invalidMeta := invalidPage.Meta() - if invalidMeta != nil { - t.Error("Expected meta to be nil, %v", invalidMeta) - } -} - -func TestPagesMetaList_NoParams(t *testing.T) { - wp := initTestClient() - - page := getAnyOnePage(t, wp) - - meta, resp, body, err := page.Meta().List(nil) - if err != nil { - t.Errorf("Should not return error: %v", err.Error()) - } - if body == nil { - t.Errorf("Should not return nil body") - } - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected 200 OK, got %v", resp.Status) - } - if meta == nil { - t.Errorf("Should not return nil meta") - } -} - -func TestPagesMetaCreate(t *testing.T) { - wp := initTestClient() - - // get a page - page := getAnyOnePage(t, wp) - - // create meta for retrieved page - m := wordpress.Meta{ - Key: "testKey", - Value: "testValue", - } - newMeta, resp, body, err := page.Meta().Create(&m) - if err != nil { - t.Errorf("Should not return error: %v", err.Error()) - } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusCreated { - t.Errorf("Expected 201 Created, got %v", resp.Status) - } - if newMeta == nil { - t.Errorf("newMeta should not be nil") - } - if newMeta.Key != m.Key { - t.Errorf("newMeta.Key should be the same, %v != %v", newMeta.Key, m.Key) - } - if newMeta.Value != m.Value { - t.Errorf("newMeta.Value should be the same, %v != %v", newMeta.Value, m.Key) - } - - // clean up - cleanUpPageMeta(t, page, newMeta.ID) -} - -func TestPagesMetaGet(t *testing.T) { - wp := initTestClient() - - // get a page - page := getAnyOnePage(t, wp) - - // create meta for retrieved page - m := wordpress.Meta{ - Key: "testKey", - Value: "testValue", - } - newMeta, resp, body, err := page.Meta().Create(&m) - if resp.StatusCode != http.StatusCreated { - t.Errorf("Expected 201 Created, got %v", resp.Status) - } - - // get meta by id for retrieved page - metaID := newMeta.ID - meta, resp, body, err := page.Meta().Get(metaID, nil) - if err != nil { - t.Errorf("Failed to get meta: %v", err.Error()) - } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { - t.Errorf("meta.Value should be the same, %v != %v", meta.Value, m.Value) - } - if meta.ID != metaID { - t.Errorf("meta.ID should be the same, %v != %v", meta.ID, metaID) - } - if newMeta.Key != m.Key { - t.Errorf("meta.Key should be the same, %v != %v", meta.Key, m.Key) - } - if newMeta.Value != m.Value { - t.Errorf("meta.Value should be the same, %v != %v", meta.Value, m.Value) - } - - // clean up - cleanUpPageMeta(t, page, newMeta.ID) -} - -func TestPagesMetaGet_Lazy(t *testing.T) { - - wp := initTestClient() - - // get a page so we can have a valid Page ID and we can create a test meta to get - page := getAnyOnePage(t, wp) - pageID := page.ID - - // create meta for retrieved page - m := wordpress.Meta{ - Key: "testKey", - Value: "testValue", - } - newMeta, resp, _, _ := page.Meta().Create(&m) - if resp.StatusCode != http.StatusCreated { - t.Errorf("Expected 201 Created, got %v", resp.Status) - } - metaID := newMeta.ID - - // Use Pages().Entity(pageID) to retrieve meta in one API call - lazyMeta, resp, body, err := wp.Pages().Entity(pageID).Meta().Get(metaID, nil) - if err != nil { - t.Errorf("Failed to lazy-get meta: %v", err.Error()) - } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { - t.Errorf("meta.Value should be the same, %v != %v", lazyMeta.Value, m.Value) - } - if lazyMeta.ID != metaID { - t.Errorf("meta.ID should be the same, %v != %v", lazyMeta.ID, metaID) - } - if lazyMeta.Key != m.Key { - t.Errorf("meta.Key should be the same, %v != %v", lazyMeta.Key, m.Key) - } - if lazyMeta.Value != m.Value { - t.Errorf("meta.Value should be the same, %v != %v", lazyMeta.Value, m.Value) - } -} - -func TestPagesMetaUpdate(t *testing.T) { - wp := initTestClient() - - // get a page - page := getAnyOnePage(t, wp) - - // create meta for retrieved page - m := wordpress.Meta{ - Key: "testKey", - Value: "testValue", - } - newMeta, resp, body, err := page.Meta().Create(&m) - if resp.StatusCode != http.StatusCreated { - t.Errorf("Expected 201 Created, got %v", resp.Status) - } - - // get meta by id for retrieved page - metaID := newMeta.ID - meta, resp, body, err := page.Meta().Get(metaID, nil) - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected 200 OK, got %v", resp.Status) - } - - // update meta by id for retrieved page - meta.Value = "newTestValue" - updatedMeta, resp, body, err := page.Meta().Update(meta.ID, meta) - if err != nil { - t.Errorf("Failed to update page meta: %v", err.Error()) - } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected 200 OK, got %v", resp.Status) - } - if updatedMeta.ID != meta.ID { - t.Errorf("updatedMeta.ID should be the same, %v != %v", updatedMeta.ID, meta.ID) - } - if updatedMeta.Key != meta.Key { - t.Errorf("updatedMeta.Key should be the same, %v != %v", updatedMeta.Key, meta.Key) - } - if updatedMeta.Value != meta.Value { - t.Errorf("updatedMeta.Value should be the same, %v != %v", updatedMeta.Value, meta.Value) - } - - // clean up - cleanUpPageMeta(t, page, newMeta.ID) -} diff --git a/pages_revisions_test.go b/pages_revisions_test.go index 99bc09e..1128fe7 100644 --- a/pages_revisions_test.go +++ b/pages_revisions_test.go @@ -1,17 +1,19 @@ package wordpress_test import ( + "context" "fmt" - "github.com/sogko/go-wordpress" "net/http" "testing" "time" + + "github.com/robbiet480/go-wordpress" ) -func getLatestRevisionForPage(t *testing.T, page *wordpress.Page) *wordpress.Revision { +func getLatestRevisionForPage(t *testing.T, ctx context.Context, page *wordpress.Page) *wordpress.Revision { - revisions, resp, _, _ := page.Revisions().List(nil) - if resp.StatusCode != http.StatusOK { + revisions, resp, _ := page.Revisions().List(ctx, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if len(revisions) < 1 { @@ -19,8 +21,8 @@ func getLatestRevisionForPage(t *testing.T, page *wordpress.Page) *wordpress.Rev } // get latest revision revisionID := revisions[0].ID - revision, resp, _, _ := page.Revisions().Get(revisionID, nil) - if resp.StatusCode != http.StatusOK { + revision, resp, _ := page.Revisions().Get(ctx, revisionID, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Fatalf("Expected 200 OK, got %v", resp.Status) } @@ -28,29 +30,27 @@ func getLatestRevisionForPage(t *testing.T, page *wordpress.Page) *wordpress.Rev } func TestPagesRevisions_InvalidCall(t *testing.T) { - // User is not allowed to call create wordpress.Page object manually to retrieve PageMetaCollection - // A proper API call would inject the right PageMetaCollection, Client and other goodies into a page, - // allowing user to call page.Revisions() + // User is not allowed to call create wordpress.Page object manually to retrieve PageMetaService + // A proper API call would inject the right PageMetaService, Client and other goodies into a page, + // allowing user to call page.Revisions invalidPage := wordpress.Page{} invalidRevisions := invalidPage.Revisions() if invalidRevisions != nil { - t.Error("Expected revisions to be nil, %v", invalidRevisions) + t.Errorf("Expected revisions to be nil, %v", invalidRevisions) } } func TestPagesRevisionsList(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - page := getAnyOnePage(t, wp) + page := getAnyOnePage(t, ctx, wp) - revisions, resp, body, err := page.Revisions().List(nil) + revisions, resp, err := page.Revisions().List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("Should not return nil body") - } - if resp.StatusCode != http.StatusOK { + + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if revisions == nil { @@ -59,20 +59,18 @@ func TestPagesRevisionsList(t *testing.T) { } func TestPagesRevisionsList_Lazy(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - page := getAnyOnePage(t, wp) + page := getAnyOnePage(t, ctx, wp) pageID := page.ID - // Use Pages().Entity(pageID) to retrieve revisions in one API call - lazyRevisions, resp, body, err := wp.Pages().Entity(pageID).Revisions().List(nil) + // Use Pages.Entity(pageID) to retrieve revisions in one API call + lazyRevisions, resp, err := wp.Pages.Entity(pageID).Revisions().List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("Should not return nil body") - } - if resp.StatusCode != http.StatusOK { + + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if lazyRevisions == nil { @@ -81,21 +79,19 @@ func TestPagesRevisionsList_Lazy(t *testing.T) { } func TestPagesRevisionsGet(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - page := getAnyOnePage(t, wp) - r := getLatestRevisionForPage(t, page) + page := getAnyOnePage(t, ctx, wp) + r := getLatestRevisionForPage(t, ctx, page) revisionID := r.ID - revision, resp, body, err := page.Revisions().Get(revisionID, nil) + revision, resp, err := page.Revisions().Get(ctx, revisionID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("Should not return nil body") - } - if resp.StatusCode != http.StatusOK { + + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if revision == nil { @@ -104,23 +100,21 @@ func TestPagesRevisionsGet(t *testing.T) { } func TestPagesRevisionsGet_Lazy(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - page := getAnyOnePage(t, wp) - r := getLatestRevisionForPage(t, page) + page := getAnyOnePage(t, ctx, wp) + r := getLatestRevisionForPage(t, ctx, page) pageID := page.ID revisionID := r.ID - // Use Pages().Entity(pageID) to retrieve revisions in one API call - lazyRevision, resp, body, err := wp.Pages().Entity(pageID).Revisions().Get(revisionID, nil) + // Use Pages.Entity(pageID) to retrieve revisions in one API call + lazyRevision, resp, err := wp.Pages.Entity(pageID).Revisions().Get(ctx, revisionID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("Should not return nil body") - } - if resp.StatusCode != http.StatusOK { + + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if lazyRevision == nil { @@ -129,9 +123,9 @@ func TestPagesRevisionsGet_Lazy(t *testing.T) { } func TestPagesRevisionsDelete_Lazy(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - page := getAnyOnePage(t, wp) + page := getAnyOnePage(t, ctx, wp) // Edit page to create a new revision // Note: wordpress would only create a new revision if there is an actual change in @@ -142,28 +136,26 @@ func TestPagesRevisionsDelete_Lazy(t *testing.T) { if originalTitle == page.Title.Raw { t.Fatalf("Flawed test, ensure that page content is modified before an update") } - updatedPage, resp, _, _ := wp.Pages().Update(page.ID, page) - if resp.StatusCode != http.StatusOK { + updatedPage, resp, _ := wp.Pages.Update(ctx, page.ID, page) + if resp != nil && resp.StatusCode != http.StatusOK { t.Fatalf("Expected 200 OK, got %v", resp.Status) } - r := getLatestRevisionForPage(t, updatedPage) + r := getLatestRevisionForPage(t, ctx, updatedPage) pageID := updatedPage.ID revisionID := r.ID - // Use Pages().Entity(pageID) to delete revisions in one API call + // Use Pages.Entity(pageID) to delete revisions in one API call // Note that deleting a revision does NOT reverse the changes made in the revision - response, resp, body, err := wp.Pages().Entity(pageID).Revisions().Delete(revisionID, nil) + response, resp, err := wp.Pages.Entity(pageID).Revisions().Delete(ctx, revisionID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("Should not return nil body") - } - if resp.StatusCode != http.StatusOK { + + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if response == false { - t.Errorf("Should not return false (bool) response") + if response == nil { + t.Errorf("Should not return nil response") } } diff --git a/pages_test.go b/pages_test.go index a0e91f0..a533dc5 100644 --- a/pages_test.go +++ b/pages_test.go @@ -1,22 +1,24 @@ package wordpress_test import ( + "context" "fmt" - "github.com/sogko/go-wordpress" "log" "net/http" "testing" + + "github.com/robbiet480/go-wordpress" ) func factoryPage() wordpress.Page { return wordpress.Page{ - Title: wordpress.Title{ + Title: wordpress.RenderedString{ Raw: "TestPagesCreate", }, - Content: wordpress.Content{ + Content: wordpress.RenderedString{ Raw: "

HEADER

Paragraph

", }, - Excerpt: wordpress.Excerpt{ + Excerpt: wordpress.RenderedString{ Raw: "

HEADER

Paragraph

", }, Type: wordpress.PostTypePage, @@ -28,15 +30,12 @@ func factoryPage() wordpress.Page { func cleanUpPage(t *testing.T, pageID int) { - wp := initTestClient() - deletedPage, resp, body, err := wp.Pages().Delete(pageID, "force=true") + wp, ctx := initTestClient() + deletedPage, resp, err := wp.Pages.Delete(ctx, pageID, "force=true") if err != nil { t.Errorf("Failed to clean up new page: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if deletedPage.ID != pageID { @@ -44,41 +43,38 @@ func cleanUpPage(t *testing.T, pageID int) { } } -func getAnyOnePage(t *testing.T, wp *wordpress.Client) *wordpress.Page { +func getAnyOnePage(t *testing.T, ctx context.Context, wp *wordpress.Client) *wordpress.Page { - pages, resp, body, err := wp.Pages().List(nil) - if resp.StatusCode != http.StatusOK { + pages, resp, err := wp.Pages.List(ctx, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if len(pages) < 1 { log.Print(err) - log.Print(body) log.Print(resp) t.Fatalf("Should not return empty pages") } pageID := pages[0].ID - page, resp, _, _ := wp.Pages().Get(pageID, "context=edit") - if resp.StatusCode != http.StatusOK { + page, resp, _ := wp.Pages.Get(ctx, pageID, "context=edit") + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } return page } func TestPagesList_NoParams(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - pages, resp, body, err := wp.Pages().List(nil) + pages, resp, err := wp.Pages.List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if pages == nil { t.Errorf("Should not return nil pages") } @@ -87,81 +83,71 @@ func TestPagesList_NoParams(t *testing.T) { } } func TestPagesList_WithParamsString(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() // assumes that API user authenticated with `edit_pages` - pages, resp, body, err := wp.Pages().List("filter[post_status]=draft") + pages, resp, err := wp.Pages.List(ctx, &wordpress.PageListOptions{Status: []string{"draft"}}) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if len(pages) != 0 { t.Errorf("Should return zero draft pages, returned %v", len(pages)) } - pages, resp, body, err = wp.Pages().List("filter[post_status]=publish") + pages, resp, err = wp.Pages.List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if len(pages) == 0 { t.Errorf("Should return at least one published pages") } } func TestPagesGet_PageExists(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - page := getAnyOnePage(t, wp) + page := getAnyOnePage(t, ctx, wp) pageID := page.ID - page, resp, body, err := wp.Pages().Get(pageID, nil) + page, resp, err := wp.Pages.Get(ctx, pageID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("body should not be nil") - } if page.ID != pageID { t.Errorf("Returned page should have the same ID as specified in Get(), %v != %v", page.ID, pageID) } } func TestPagesGet_PageDoesNotExists(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() pageID := -1 - _, resp, body, err := wp.Pages().Get(pageID, nil) + _, resp, err := wp.Pages.Get(ctx, pageID, nil) if err == nil { t.Errorf("Should return error") } - if resp.StatusCode != http.StatusNotFound { + if resp != nil && resp.StatusCode != http.StatusNotFound { t.Errorf("Expected 400 NotFound, got %v", resp.Status) } - if body == nil { - t.Errorf("body should not be nil") - } } func TestPagesGet_Lazy(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - page := getAnyOnePage(t, wp) + page := getAnyOnePage(t, ctx, wp) pageID := page.ID - //The proper way to get lazy-fetch pages. Pages().Entity() won't make any HTTP request - lazyPage := wp.Pages().Entity(pageID) + //The proper way to get lazy-fetch pages. Pages.Entityctx, () won't make any HTTP request + lazyPage := wp.Pages.Entity(pageID) if lazyPage == nil { t.Errorf("lazyPage should not be nil") } @@ -173,16 +159,13 @@ func TestPagesGet_Lazy(t *testing.T) { } // populate Page Entity - page, resp, body, err := lazyPage.Populate(nil) + page, resp, err := lazyPage.Populate(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("body should not be nil") - } if page.ID != pageID { t.Errorf("Returned page should have the same ID as specified in Get(), %v != %v", page.ID, pageID) } @@ -192,19 +175,16 @@ func TestPagesGet_Lazy(t *testing.T) { } func TestPagesCreate(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() p := factoryPage() - newPage, resp, body, err := wp.Pages().Create(&p) + newPage, resp, err := wp.Pages.Create(ctx, &p) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusCreated { + if resp != nil && resp.StatusCode != http.StatusCreated { t.Errorf("Expected 201 Created, got %v", resp.Status) } - if body == nil { - t.Errorf("body should not be nil") - } if newPage == nil { t.Errorf("newPage should not be nil") } @@ -226,18 +206,18 @@ func TestPagesCreate(t *testing.T) { } func TestPagesUpdate(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() // create a new page first p := factoryPage() - newPage, resp, _, _ := wp.Pages().Create(&p) - if resp.StatusCode != http.StatusCreated { + newPage, resp, _ := wp.Pages.Create(ctx, &p) + if resp != nil && resp.StatusCode != http.StatusCreated { t.Fatalf("Expected 201 Created, got %v", resp.Status) } // get the page in `edit` context - page, resp, _, _ := wp.Pages().Get(newPage.ID, "context=edit") - if resp.StatusCode != http.StatusOK { + page, resp, _ := wp.Pages.Get(ctx, newPage.ID, "context=edit") + if resp != nil && resp.StatusCode != http.StatusOK { t.Fatalf("Expected 200 OK, got %v", resp.Status) } @@ -249,16 +229,13 @@ func TestPagesUpdate(t *testing.T) { page.Title.Raw = newTitle // update page - updatePage, resp, body, err := wp.Pages().Update(page.ID, page) + updatePage, resp, err := wp.Pages.Update(ctx, page.ID, page) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("body should not be nil") - } if updatePage == nil { t.Errorf("updatePage should not be nil") } @@ -271,28 +248,25 @@ func TestPagesUpdate(t *testing.T) { } func TestPagesDelete_NoParams_MoveToTrash(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() // create a new page first p := factoryPage() - newPage, resp, _, _ := wp.Pages().Create(&p) - if resp.StatusCode != http.StatusCreated { + newPage, resp, _ := wp.Pages.Create(ctx, &p) + if resp != nil && resp.StatusCode != http.StatusCreated { t.Errorf("Expected 201 Created, got %v", resp.Status) } // delete page (move to trash) - deletedPage, resp, body, err := wp.Pages().Delete(newPage.ID, nil) + deletedPage, resp, err := wp.Pages.Delete(ctx, newPage.ID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("body should not be nil") - } if deletedPage == nil { - t.Errorf("updatePage should not be nil") + t.Errorf("deletedPage should not be nil") } if deletedPage.ID != newPage.ID { t.Errorf("Deleted page ID should be the same as created page: %v != %v", deletedPage.ID, newPage.ID) @@ -302,26 +276,23 @@ func TestPagesDelete_NoParams_MoveToTrash(t *testing.T) { cleanUpPage(t, newPage.ID) } func TestPagesDelete_WithParams_DeletePermanently(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() // create a new page first p := factoryPage() - newPage, resp, _, _ := wp.Pages().Create(&p) - if resp.StatusCode != http.StatusCreated { + newPage, resp, _ := wp.Pages.Create(ctx, &p) + if resp != nil && resp.StatusCode != http.StatusCreated { t.Errorf("Expected 201 Created, got %v", resp.Status) } // delete page (delete permanently) - deletedPage, resp, body, err := wp.Pages().Delete(newPage.ID, "force=true") + deletedPage, resp, err := wp.Pages.Delete(ctx, newPage.ID, "force=true") if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } - if body == nil { - t.Errorf("body should not be nil") - } if deletedPage == nil { t.Errorf("updatePage should not be nil") } diff --git a/posts.go b/posts.go index e082b7a..bd69fcb 100644 --- a/posts.go +++ b/posts.go @@ -1,10 +1,12 @@ package wordpress import ( + "context" "fmt" - "net/http" + "log" ) +// Constants for different post values. const ( PostStatusDraft = "draft" PostStatusPending = "pending" @@ -35,155 +37,162 @@ const ( PostFormatChat = "chat" ) -type GUID struct { - Raw string `json:"raw,omitempty"` - Rendered string `json:"rendered,omitempty"` -} -type Title struct { - Raw string `json:"raw,omitempty"` - Rendered string `json:"rendered,omitempty"` -} -type Content struct { - Raw string `json:"raw,omitempty"` - Rendered string `json:"rendered,omitempty"` -} -type Excerpt struct { +// RenderedString contains a raw and rendered version of a string such as title, content, excerpt, etc. +type RenderedString struct { Raw string `json:"raw,omitempty"` Rendered string `json:"rendered,omitempty"` } +// Post represents a WordPress post. type Post struct { - collection *PostsCollection `json:"-,omitempty"` - - ID int `json:"id,omitempty"` - Date string `json:"date,omitempty"` - DateGMT string `json:"date_gmt,omitempty"` - GUID GUID `json:"guid,omitempty"` - Link string `json:"link,omitempty"` - Modified string `json:"modified,omitempty"` - ModifiedGMT string `json:"modifiedGMT,omitempty"` - Password string `json:"password,omitempty"` - Slug string `json:"slug,omitempty"` - Status string `json:"status,omitempty"` - Type string `json:"type,omitempty"` - Title Title `json:"title,omitempty"` - Content Content `json:"content,omitempty"` - Author int `json:"author,omitempty"` - Excerpt Excerpt `json:"excerpt,omitempty"` - FeaturedImage int `json:"featured_image,omitempty"` - CommentStatus string `json:"comment_status,omitempty"` - PingStatus string `json:"ping_status,omitempty"` - Format string `json:"format,omitempty"` - Sticky bool `json:"sticky,omitempty"` -} - -func (entity *Post) setCollection(col *PostsCollection) { - entity.collection = col -} -func (entity *Post) Meta() *MetaCollection { - if entity.collection == nil { - // missing post.collection parent. Probably Post struct was initialized manually. - _warning("Missing parent post collection") - return nil - } - return &MetaCollection{ - client: entity.collection.client, - parent: entity, - parentType: CollectionPosts, - url: fmt.Sprintf("%v/%v/%v", entity.collection.url, entity.ID, CollectionMeta), - } -} -func (entity *Post) Revisions() *RevisionsCollection { + collection *PostsService + + Author int `json:"author,omitempty"` + Categories []int `json:"categories,omitempty"` + CommentStatus string `json:"comment_status,omitempty"` + Content RenderedString `json:"content,omitempty"` + Date Time `json:"date,omitempty"` + DateGMT Time `json:"date_gmt,omitempty"` + Excerpt RenderedString `json:"excerpt,omitempty"` + FeaturedMedia int `json:"featured_media,omitempty"` + Format string `json:"format,omitempty"` + GUID RenderedString `json:"guid,omitempty"` + ID int `json:"id,omitempty"` + Link string `json:"link,omitempty"` + Modified Time `json:"modified,omitempty"` + ModifiedGMT Time `json:"modified_gmt,omitempty"` + Password string `json:"password,omitempty"` + PingStatus string `json:"ping_status,omitempty"` + Slug string `json:"slug,omitempty"` + Status string `json:"status,omitempty"` + Sticky bool `json:"sticky,omitempty"` + Subtitle string `json:"wps_subtitle,omitempty"` + Tags []int `json:"tags,omitempty"` + Template string `json:"template,omitempty"` + Title RenderedString `json:"title,omitempty"` + Type string `json:"type,omitempty"` +} + +func (entity *Post) setService(c *PostsService) { + entity.collection = c +} + +// Revisions gets the revisions of a single post. +func (entity *Post) Revisions() *RevisionsService { if entity.collection == nil { // missing post.collection parent. Probably Post struct was initialized manually, not fetched from API - _warning("Missing parent post collection") + log.Println("[go-wordpress] Missing parent post collection") return nil } - return &RevisionsCollection{ - client: entity.collection.client, + return &RevisionsService{ + service: service(*entity.collection), parent: entity, - parentType: CollectionPosts, - url: fmt.Sprintf("%v/%v/%v", entity.collection.url, entity.ID, CollectionRevisions), + parentType: "posts", + url: fmt.Sprintf("%v/%v/%v", "posts", entity.ID, "revisions"), } } -func (entity *Post) Terms() *PostsTermsCollection { + +// Terms gets the terms of a single post. +func (entity *Post) Terms() *PostsTermsService { if entity.collection == nil { // missing post.collection parent. Probably Post struct was initialized manually, not fetched from API - _warning("Missing parent post collection") + log.Println("[go-wordpress] Missing parent post collection") return nil } - return &PostsTermsCollection{ + return &PostsTermsService{ client: entity.collection.client, parent: entity, - parentType: CollectionPosts, - url: fmt.Sprintf("%v/%v/%v", entity.collection.url, entity.ID, CollectionTerms), + parentType: "posts", + url: fmt.Sprintf("%v/%v/%v", "posts", entity.ID, "terms"), } } -func (entity *Post) Populate(params interface{}) (*Post, *http.Response, []byte, error) { - return entity.collection.Get(entity.ID, params) -} -type PostsCollection struct { - client *Client - url string - entityURL string +// Populate will fill a manually initialized post with the collection information. +func (entity *Post) Populate(ctx context.Context, params interface{}) (*Post, *Response, error) { + return entity.collection.Get(ctx, entity.ID, params) } -func (col *PostsCollection) List(params interface{}) ([]Post, *http.Response, []byte, error) { - var posts []Post - resp, body, err := col.client.List(col.url, params, &posts) +// PostsService provides access to the post related functions in the WordPress REST API. +type PostsService service + +// List returns a list of posts. +func (c *PostsService) List(ctx context.Context, opts *PostListOptions) ([]*Post, *Response, error) { + u, err := addOptions("posts", opts) + if err != nil { + return nil, nil, err + } + + req, err := c.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + posts := []*Post{} + resp, err := c.client.Do(ctx, req, &posts) + if err != nil { + return nil, resp, err + } // set collection object for each entity which has sub-collection for _, p := range posts { - p.setCollection(col) + p.setService(c) } - return posts, resp, body, err + return posts, resp, nil } -func (col *PostsCollection) Create(new *Post) (*Post, *http.Response, []byte, error) { + +// Create creates a new post. +func (c *PostsService) Create(ctx context.Context, newPost *Post) (*Post, *Response, error) { var created Post - resp, body, err := col.client.Create(col.url, new, &created) + resp, err := c.client.Create(ctx, "posts", newPost, &created) - created.setCollection(col) + created.setService(c) - return &created, resp, body, err + return &created, resp, err } -func (col *PostsCollection) Get(id int, params interface{}) (*Post, *http.Response, []byte, error) { + +// Get returns a single post for the given id. +func (c *PostsService) Get(ctx context.Context, id int, params interface{}) (*Post, *Response, error) { var entity Post - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Get(entityURL, params, &entity) + entityURL := fmt.Sprintf("posts/%v", id) + resp, err := c.client.Get(ctx, entityURL, params, &entity) // set collection object for each entity which has sub-collection - entity.setCollection(col) + entity.setService(c) - return &entity, resp, body, err + return &entity, resp, err } -func (col *PostsCollection) Entity(id int) *Post { + +// Entity returns a basic post for the given id. +func (c *PostsService) Entity(id int) *Post { entity := Post{ - collection: col, + collection: c, ID: id, } return &entity } -func (col *PostsCollection) Update(id int, post *Post) (*Post, *http.Response, []byte, error) { +// Update updates a single post with the given id. +func (c *PostsService) Update(ctx context.Context, id int, post *Post) (*Post, *Response, error) { var updated Post - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Update(entityURL, post, &updated) + entityURL := fmt.Sprintf("posts/%v", id) + resp, err := c.client.Update(ctx, entityURL, post, &updated) // set collection object for each entity which has sub-collection - updated.setCollection(col) + updated.setService(c) - return &updated, resp, body, err + return &updated, resp, err } -func (col *PostsCollection) Delete(id int, params interface{}) (*Post, *http.Response, []byte, error) { + +// Delete removes the post with the given id. +func (c *PostsService) Delete(ctx context.Context, id int, params interface{}) (*Post, *Response, error) { var deleted Post - entityURL := fmt.Sprintf("%v/%v", col.url, id) + entityURL := fmt.Sprintf("posts/%v", id) - resp, body, err := col.client.Delete(entityURL, params, &deleted) + resp, err := c.client.Delete(ctx, entityURL, params, &deleted) // set collection object for each entity which has sub-collection - deleted.setCollection(col) + deleted.setService(c) - return &deleted, resp, body, err + return &deleted, resp, err } diff --git a/posts_meta_test.go b/posts_meta_test.go deleted file mode 100644 index 91e98e9..0000000 --- a/posts_meta_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package wordpress_test - -import ( - "github.com/sogko/go-wordpress" - "net/http" - "testing" -) - -func cleanUpPostMeta(t *testing.T, post *wordpress.Post, metaId int) { - - // note: Need to pass in `force=true` param in order to delete post meta - deletedMeta, resp, body, err := post.Meta().Delete(metaId, "force=true") - if err != nil { - t.Errorf("Failed to clean up new post: %v", err.Error()) - } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected 200 OK, got %v", resp.Status) - } - if deletedMeta.Message != "Deleted meta" { - t.Errorf("Unexpected response to deleted meta: %v", deletedMeta.Message) - } - -} - -func TestPostsMeta_InvalidCall(t *testing.T) { - // User is not allowed to call create wordpress.Post object manually to retrieve PostMetaCollection - // A proper API call would inject the right PostMetaCollection, Client and other goodies into a post, - // allowing user to call post.Meta() - invalidPost := wordpress.Post{} - invalidMeta := invalidPost.Meta() - if invalidMeta != nil { - t.Error("Expected meta to be nil, %v", invalidMeta) - } -} - -func TestPostsMetaList_NoParams(t *testing.T) { - wp := initTestClient() - - post := getAnyOnePost(t, wp) - - meta, resp, body, err := post.Meta().List(nil) - if err != nil { - t.Errorf("Should not return error: %v", err.Error()) - } - if body == nil { - t.Errorf("Should not return nil body") - } - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected 200 OK, got %v", resp.Status) - } - if meta == nil { - t.Errorf("Should not return nil meta") - } -} - -func TestPostsMetaCreate(t *testing.T) { - wp := initTestClient() - - // get a post - post := getAnyOnePost(t, wp) - - // create meta for retrieved post - m := wordpress.Meta{ - Key: "testKey", - Value: "testValue", - } - newMeta, resp, body, err := post.Meta().Create(&m) - if err != nil { - t.Errorf("Should not return error: %v", err.Error()) - } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusCreated { - t.Errorf("Expected 201 Created, got %v", resp.Status) - } - if newMeta == nil { - t.Errorf("newMeta should not be nil") - } - if newMeta.Key != m.Key { - t.Errorf("newMeta.Key should be the same, %v != %v", newMeta.Key, m.Key) - } - if newMeta.Value != m.Value { - t.Errorf("newMeta.Value should be the same, %v != %v", newMeta.Value, m.Key) - } - - // clean up - cleanUpPostMeta(t, post, newMeta.ID) -} - -func TestPostsMetaGet(t *testing.T) { - wp := initTestClient() - - // get a post - post := getAnyOnePost(t, wp) - - // create meta for retrieved post - m := wordpress.Meta{ - Key: "testKey", - Value: "testValue", - } - newMeta, resp, body, err := post.Meta().Create(&m) - if resp.StatusCode != http.StatusCreated { - t.Errorf("Expected 201 Created, got %v", resp.Status) - } - - // get meta by id for retrieved post - metaID := newMeta.ID - meta, resp, body, err := post.Meta().Get(metaID, nil) - if err != nil { - t.Errorf("Failed to get meta: %v", err.Error()) - } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { - t.Errorf("meta.Value should be the same, %v != %v", meta.Value, m.Value) - } - if meta.ID != metaID { - t.Errorf("meta.ID should be the same, %v != %v", meta.ID, metaID) - } - if newMeta.Key != m.Key { - t.Errorf("meta.Key should be the same, %v != %v", meta.Key, m.Key) - } - if newMeta.Value != m.Value { - t.Errorf("meta.Value should be the same, %v != %v", meta.Value, m.Value) - } - - // clean up - cleanUpPostMeta(t, post, newMeta.ID) -} - -func TestPostsMetaGet_Lazy(t *testing.T) { - - wp := initTestClient() - - // get a post so we can have a valid Post ID and we can create a test meta to get - post := getAnyOnePost(t, wp) - postID := post.ID - - // create meta for retrieved post - m := wordpress.Meta{ - Key: "testKey", - Value: "testValue", - } - newMeta, resp, _, _ := post.Meta().Create(&m) - if resp.StatusCode != http.StatusCreated { - t.Errorf("Expected 201 Created, got %v", resp.Status) - } - metaID := newMeta.ID - - // Use Posts().Entity(postID) to retrieve meta in one API call - lazyMeta, resp, body, err := wp.Posts().Entity(postID).Meta().Get(metaID, nil) - if err != nil { - t.Errorf("Failed to lazy-get meta: %v", err.Error()) - } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { - t.Errorf("meta.Value should be the same, %v != %v", lazyMeta.Value, m.Value) - } - if lazyMeta.ID != metaID { - t.Errorf("meta.ID should be the same, %v != %v", lazyMeta.ID, metaID) - } - if lazyMeta.Key != m.Key { - t.Errorf("meta.Key should be the same, %v != %v", lazyMeta.Key, m.Key) - } - if lazyMeta.Value != m.Value { - t.Errorf("meta.Value should be the same, %v != %v", lazyMeta.Value, m.Value) - } -} - -func TestPostsMetaUpdate(t *testing.T) { - wp := initTestClient() - - // get a post - post := getAnyOnePost(t, wp) - - // create meta for retrieved post - m := wordpress.Meta{ - Key: "testKey", - Value: "testValue", - } - newMeta, resp, body, err := post.Meta().Create(&m) - if resp.StatusCode != http.StatusCreated { - t.Errorf("Expected 201 Created, got %v", resp.Status) - } - - // get meta by id for retrieved post - metaID := newMeta.ID - meta, resp, body, err := post.Meta().Get(metaID, nil) - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected 200 OK, got %v", resp.Status) - } - - // update meta by id for retrieved post - meta.Value = "newTestValue" - updatedMeta, resp, body, err := post.Meta().Update(meta.ID, meta) - if err != nil { - t.Errorf("Failed to update post meta: %v", err.Error()) - } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected 200 OK, got %v", resp.Status) - } - if updatedMeta.ID != meta.ID { - t.Errorf("updatedMeta.ID should be the same, %v != %v", updatedMeta.ID, meta.ID) - } - if updatedMeta.Key != meta.Key { - t.Errorf("updatedMeta.Key should be the same, %v != %v", updatedMeta.Key, meta.Key) - } - if updatedMeta.Value != meta.Value { - t.Errorf("updatedMeta.Value should be the same, %v != %v", updatedMeta.Value, meta.Value) - } - - // clean up - cleanUpPostMeta(t, post, newMeta.ID) -} diff --git a/posts_revisions_test.go b/posts_revisions_test.go index fdeb1c2..4e26df3 100644 --- a/posts_revisions_test.go +++ b/posts_revisions_test.go @@ -1,17 +1,19 @@ package wordpress_test import ( + "context" "fmt" - "github.com/sogko/go-wordpress" "net/http" "testing" "time" + + "github.com/robbiet480/go-wordpress" ) -func getLatestRevisionForPost(t *testing.T, post *wordpress.Post) *wordpress.Revision { +func getLatestRevisionForPost(t *testing.T, ctx context.Context, post *wordpress.Post) *wordpress.Revision { - revisions, resp, _, _ := post.Revisions().List(nil) - if resp.StatusCode != http.StatusOK { + revisions, resp, _ := post.Revisions().List(ctx, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if len(revisions) < 1 { @@ -19,8 +21,8 @@ func getLatestRevisionForPost(t *testing.T, post *wordpress.Post) *wordpress.Rev } // get latest revision revisionID := revisions[0].ID - revision, resp, _, _ := post.Revisions().Get(revisionID, nil) - if resp.StatusCode != http.StatusOK { + revision, resp, _ := post.Revisions().Get(ctx, revisionID, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Fatalf("Expected 200 OK, got %v", resp.Status) } @@ -28,29 +30,27 @@ func getLatestRevisionForPost(t *testing.T, post *wordpress.Post) *wordpress.Rev } func TestPostsRevisions_InvalidCall(t *testing.T) { - // User is not allowed to call create wordpress.Post object manually to retrieve PostMetaCollection - // A proper API call would inject the right PostMetaCollection, Client and other goodies into a post, + // User is not allowed to call create wordpress.Post object manually to retrieve PostMetaService + // A proper API call would inject the right PostMetaService, Client and other goodies into a post, // allowing user to call post.Revisions() invalidPost := wordpress.Post{} invalidRevisions := invalidPost.Revisions() if invalidRevisions != nil { - t.Error("Expected revisions to be nil, %v", invalidRevisions) + t.Errorf("Expected revisions to be nil, %v", invalidRevisions) } } func TestPostsRevisionsList(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - post := getAnyOnePost(t, wp) + post := getAnyOnePost(t, ctx, wp) - revisions, resp, body, err := post.Revisions().List(nil) + revisions, resp, err := post.Revisions().List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("Should not return nil body") - } - if resp.StatusCode != http.StatusOK { + + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if revisions == nil { @@ -59,20 +59,18 @@ func TestPostsRevisionsList(t *testing.T) { } func TestPostsRevisionsList_Lazy(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - post := getAnyOnePost(t, wp) + post := getAnyOnePost(t, ctx, wp) postID := post.ID - // Use Posts().Entity(postID) to retrieve revisions in one API call - lazyRevisions, resp, body, err := wp.Posts().Entity(postID).Revisions().List(nil) + // Use Posts.Entity(postID) to retrieve revisions in one API call + lazyRevisions, resp, err := wp.Posts.Entity(postID).Revisions().List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("Should not return nil body") - } - if resp.StatusCode != http.StatusOK { + + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if lazyRevisions == nil { @@ -81,21 +79,19 @@ func TestPostsRevisionsList_Lazy(t *testing.T) { } func TestPostsRevisionsGet(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - post := getAnyOnePost(t, wp) - r := getLatestRevisionForPost(t, post) + post := getAnyOnePost(t, ctx, wp) + r := getLatestRevisionForPost(t, ctx, post) revisionID := r.ID - revision, resp, body, err := post.Revisions().Get(revisionID, nil) + revision, resp, err := post.Revisions().Get(ctx, revisionID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("Should not return nil body") - } - if resp.StatusCode != http.StatusOK { + + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if revision == nil { @@ -104,23 +100,21 @@ func TestPostsRevisionsGet(t *testing.T) { } func TestPostsRevisionsGet_Lazy(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - post := getAnyOnePost(t, wp) - r := getLatestRevisionForPost(t, post) + post := getAnyOnePost(t, ctx, wp) + r := getLatestRevisionForPost(t, ctx, post) postID := post.ID revisionID := r.ID - // Use Posts().Entity(postID) to retrieve revisions in one API call - lazyRevision, resp, body, err := wp.Posts().Entity(postID).Revisions().Get(revisionID, nil) + // Use Posts.Entity(postID) to retrieve revisions in one API call + lazyRevision, resp, err := wp.Posts.Entity(postID).Revisions().Get(ctx, revisionID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("Should not return nil body") - } - if resp.StatusCode != http.StatusOK { + + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if lazyRevision == nil { @@ -129,9 +123,9 @@ func TestPostsRevisionsGet_Lazy(t *testing.T) { } func TestPostsRevisionsDelete_Lazy(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - post := getAnyOnePost(t, wp) + post := getAnyOnePost(t, ctx, wp) // Edit post to create a new revision // Note: wordpress would only create a new revision if there is an actual change in @@ -142,28 +136,26 @@ func TestPostsRevisionsDelete_Lazy(t *testing.T) { if originalTitle == post.Title.Raw { t.Fatalf("Flawed test, ensure that post content is modified before an update") } - updatedPost, resp, _, _ := wp.Posts().Update(post.ID, post) - if resp.StatusCode != http.StatusOK { + updatedPost, resp, _ := wp.Posts.Update(ctx, post.ID, post) + if resp != nil && resp.StatusCode != http.StatusOK { t.Fatalf("Expected 200 OK, got %v", resp.Status) } - r := getLatestRevisionForPost(t, updatedPost) + r := getLatestRevisionForPost(t, ctx, updatedPost) postID := updatedPost.ID revisionID := r.ID - // Use Posts().Entity(postID) to delete revisions in one API call + // Use Posts.Entity(postID) to delete revisions in one API call // Note that deleting a revision does NOT reverse the changes made in the revision - response, resp, body, err := wp.Posts().Entity(postID).Revisions().Delete(revisionID, nil) + response, resp, err := wp.Posts.Entity(postID).Revisions().Delete(ctx, revisionID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("Should not return nil body") - } - if resp.StatusCode != http.StatusOK { + + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if response == false { - t.Errorf("Should not return false (bool) response") + if response == nil { + t.Errorf("Should not return nil response") } } diff --git a/posts_terms.go b/posts_terms.go index e65ebd0..10d1cff 100644 --- a/posts_terms.go +++ b/posts_terms.go @@ -1,10 +1,11 @@ package wordpress import ( + "context" "fmt" - "net/http" ) +// PostsTerm represents a WordPress post post term. type PostsTerm struct { ID int `json:"id,omitempty"` Count int `json:"integer,omitempty"` @@ -16,60 +17,74 @@ type PostsTerm struct { Parent int `json:"parent,omitempty"` } -type PostsTermsCollection struct { +// PostsTermsService provides access to the post term related functions in the WordPress REST API. +type PostsTermsService struct { client *Client url string parent interface{} parentType string } -func (col *PostsTermsCollection) List(taxonomy string, params interface{}) ([]PostsTerm, *http.Response, []byte, error) { - var terms []PostsTerm - url := fmt.Sprintf("%v/%v", col.url, taxonomy) - resp, body, err := col.client.List(url, params, &terms) - return terms, resp, body, err +// List returns a list of post terms. +func (c *PostsTermsService) List(ctx context.Context, taxonomy string, params interface{}) ([]*PostsTerm, *Response, error) { + var terms []*PostsTerm + url := fmt.Sprintf("%v/%v", c.url, taxonomy) + resp, err := c.client.List(ctx, url, params, &terms) + return terms, resp, err } -func (col *PostsTermsCollection) Tag() *PostsTermsTaxonomyCollection { - return &PostsTermsTaxonomyCollection{ - client: col.client, - url: fmt.Sprintf("%v/tag", col.url), + +// Tag returns the tags of a post. +func (c *PostsTermsService) Tag() *PostsTermsTaxonomyService { + return &PostsTermsTaxonomyService{ + client: c.client, + url: fmt.Sprintf("%v/tag", c.url), taxonomyBase: "tag", } } -func (col *PostsTermsCollection) Category() *PostsTermsTaxonomyCollection { - return &PostsTermsTaxonomyCollection{ - client: col.client, - url: fmt.Sprintf("%v/category", col.url), + +// Category returns the categories of a post. +func (c *PostsTermsService) Category() *PostsTermsTaxonomyService { + return &PostsTermsTaxonomyService{ + client: c.client, + url: fmt.Sprintf("%v/category", c.url), taxonomyBase: "category", } } -type PostsTermsTaxonomyCollection struct { +// PostsTermsTaxonomyService contains data about the post terms taxonomy service +type PostsTermsTaxonomyService struct { client *Client url string taxonomyBase string } -func (col *PostsTermsTaxonomyCollection) List(params interface{}) ([]PostsTerm, *http.Response, []byte, error) { - var terms []PostsTerm - resp, body, err := col.client.List(col.url, params, &terms) - return terms, resp, body, err +// List returns a list of post terms. +func (c *PostsTermsTaxonomyService) List(ctx context.Context, params interface{}) ([]*PostsTerm, *Response, error) { + var terms []*PostsTerm + resp, err := c.client.List(ctx, c.url, params, &terms) + return terms, resp, err } -func (col *PostsTermsTaxonomyCollection) Create(id int) (*PostsTerm, *http.Response, []byte, error) { + +// Create creates a new post term. +func (c *PostsTermsTaxonomyService) Create(ctx context.Context, id int) (*PostsTerm, *Response, error) { var created PostsTerm - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Create(entityURL, nil, &created) - return &created, resp, body, err + entityURL := fmt.Sprintf("%v/%v", c.url, id) + resp, err := c.client.Create(ctx, entityURL, nil, &created) + return &created, resp, err } -func (col *PostsTermsTaxonomyCollection) Get(id int, params interface{}) (*PostsTerm, *http.Response, []byte, error) { + +// Get returns a single post term for the given id. +func (c *PostsTermsTaxonomyService) Get(ctx context.Context, id int, params interface{}) (*PostsTerm, *Response, error) { var entity PostsTerm - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Get(entityURL, params, &entity) - return &entity, resp, body, err + entityURL := fmt.Sprintf("%v/%v", c.url, id) + resp, err := c.client.Get(ctx, entityURL, params, &entity) + return &entity, resp, err } -func (col *PostsTermsTaxonomyCollection) Delete(id int, params interface{}) (*PostsTerm, *http.Response, []byte, error) { + +// Delete removes the post term with the given id. +func (c *PostsTermsTaxonomyService) Delete(ctx context.Context, id int, params interface{}) (*PostsTerm, *Response, error) { var deleted PostsTerm - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Delete(entityURL, params, &deleted) - return &deleted, resp, body, err + entityURL := fmt.Sprintf("%v/%v", c.url, id) + resp, err := c.client.Delete(ctx, entityURL, params, &deleted) + return &deleted, resp, err } diff --git a/posts_terms_category_test.go b/posts_terms_category_test.go index c84b7bf..78629d2 100644 --- a/posts_terms_category_test.go +++ b/posts_terms_category_test.go @@ -1,23 +1,22 @@ package wordpress_test import ( - "github.com/sogko/go-wordpress" + "context" "net/http" "testing" + + "github.com/robbiet480/go-wordpress" ) func cleanUpPostsTermsCategory(t *testing.T, postID int, id int) { - wp := initTestClient() + wp, ctx := initTestClient() // terms does not support trashing - deletedTerm, resp, body, err := wp.Posts().Entity(postID).Terms().Category().Delete(id, "force=true") + deletedTerm, resp, err := wp.Posts.Entity(postID).Terms().Category().Delete(ctx, id, "force=true") if err != nil { t.Errorf("Failed to clean up new term: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if deletedTerm.ID != id { @@ -25,10 +24,10 @@ func cleanUpPostsTermsCategory(t *testing.T, postID int, id int) { } } -func getAnyOnePostsTermsCategory(t *testing.T, wp *wordpress.Client, postID int) *wordpress.PostsTerm { +func getAnyOnePostsTermsCategory(t *testing.T, ctx context.Context, wp *wordpress.Client, postID int) *wordpress.PostsTerm { - terms, resp, _, _ := wp.Posts().Entity(postID).Terms().Category().List(nil) - if resp.StatusCode != http.StatusOK { + terms, resp, _ := wp.Posts.Entity(postID).Terms().Category().List(ctx, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if len(terms) < 1 { @@ -37,8 +36,8 @@ func getAnyOnePostsTermsCategory(t *testing.T, wp *wordpress.Client, postID int) id := terms[0].ID - term, resp, _, _ := wp.Posts().Entity(postID).Terms().Category().Get(id, nil) - if resp.StatusCode != http.StatusOK { + term, resp, _ := wp.Posts.Entity(postID).Terms().Category().Get(ctx, id, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Fatalf("Expected 200 OK, got %v", resp.Status) } @@ -46,30 +45,27 @@ func getAnyOnePostsTermsCategory(t *testing.T, wp *wordpress.Client, postID int) } func TestPostsTermsCategory_InvalidCall(t *testing.T) { - // User is not allowed to call create wordpress.Post object manually to retrieve PostsTermsCollection - // A proper API call would inject the right PostsTermsCollection, Client and other goodies into a post, + // User is not allowed to call create wordpress.Post object manually to retrieve PostsTermsService + // A proper API call would inject the right PostsTermsService, Client and other goodies into a post, // allowing user to call post.Terms() invalidPost := wordpress.Post{} invalidTerms := invalidPost.Terms() if invalidTerms != nil { - t.Error("Expected meta to be nil, %v", invalidTerms) + t.Errorf("Expected meta to be nil, %v", invalidTerms) } } func TestPostsTermsCategoryList(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() - post := getAnyOnePost(t, wp) + wp, ctx := initTestClient() + post := getAnyOnePost(t, ctx, wp) postID := post.ID - terms, resp, body, err := wp.Posts().Entity(postID).Terms().Category().List(nil) + terms, resp, err := wp.Posts.Entity(postID).Terms().Category().List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if terms == nil { @@ -80,19 +76,16 @@ func TestPostsTermsCategoryList(t *testing.T) { func TestPostsTermsCategoryGet(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() - post := getAnyOnePost(t, wp) + wp, ctx := initTestClient() + post := getAnyOnePost(t, ctx, wp) postID := post.ID - tt := getAnyOnePostsTermsCategory(t, wp, postID) + tt := getAnyOnePostsTermsCategory(t, ctx, wp, postID) - term, resp, body, err := wp.Posts().Entity(postID).Terms().Category().Get(tt.ID, nil) + term, resp, err := wp.Posts.Entity(postID).Terms().Category().Get(ctx, tt.ID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if term == nil { @@ -104,20 +97,17 @@ func TestPostsTermsCategoryGet(t *testing.T) { func TestPostsTermsCategoryCreate_Existing(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() - post := getAnyOnePost(t, wp) - tt := getAnyOneTermsCategory(t, wp) + wp, ctx := initTestClient() + post := getAnyOnePost(t, ctx, wp) + tt := getAnyOneTermsCategory(t, ctx, wp) postID := post.ID termID := tt.ID - term, resp, body, err := wp.Posts().Entity(postID).Terms().Category().Create(termID) + term, resp, err := wp.Posts.Entity(postID).Terms().Category().Create(ctx, termID) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusCreated { + if resp != nil && resp.StatusCode != http.StatusCreated { t.Errorf("Expected 201 Created, got %v", resp.Status) } if term == nil { @@ -131,15 +121,15 @@ func TestPostsTermsCategoryCreate_Existing(t *testing.T) { func TestPostsTermsCategoryDelete(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() - post := getAnyOnePost(t, wp) - tt := getAnyOneTermsCategory(t, wp) + wp, ctx := initTestClient() + post := getAnyOnePost(t, ctx, wp) + tt := getAnyOneTermsCategory(t, ctx, wp) postID := post.ID termID := tt.ID // create category - newTerm, resp, _, _ := wp.Posts().Entity(postID).Terms().Category().Create(termID) - if resp.StatusCode != http.StatusCreated { + newTerm, resp, _ := wp.Posts.Entity(postID).Terms().Category().Create(ctx, termID) + if resp != nil && resp.StatusCode != http.StatusCreated { t.Errorf("Expected 201 Created, got %v", resp.Status) } if newTerm == nil { @@ -148,14 +138,11 @@ func TestPostsTermsCategoryDelete(t *testing.T) { // delete category // Note: Terms does not support trashing; `force=true` is required - deletedTerm, resp, body, err := wp.Posts().Entity(postID).Terms().Category().Delete(newTerm.ID, "force=true") + deletedTerm, resp, err := wp.Posts.Entity(postID).Terms().Category().Delete(ctx, newTerm.ID, "force=true") if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if deletedTerm == nil { diff --git a/posts_terms_tag_test.go b/posts_terms_tag_test.go index 4ded790..d7387f1 100644 --- a/posts_terms_tag_test.go +++ b/posts_terms_tag_test.go @@ -1,23 +1,22 @@ package wordpress_test import ( - "github.com/sogko/go-wordpress" + "context" "net/http" "testing" + + "github.com/robbiet480/go-wordpress" ) func cleanUpPostsTermsTag(t *testing.T, postID int, id int) { - wp := initTestClient() + wp, ctx := initTestClient() // terms does not support trashing - deletedTerm, resp, body, err := wp.Posts().Entity(postID).Terms().Tag().Delete(id, "force=true") + deletedTerm, resp, err := wp.Posts.Entity(postID).Terms().Tag().Delete(ctx, id, "force=true") if err != nil { t.Errorf("Failed to clean up new term: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if deletedTerm.ID != id { @@ -25,10 +24,10 @@ func cleanUpPostsTermsTag(t *testing.T, postID int, id int) { } } -func getAnyOnePostsTermsTag(t *testing.T, wp *wordpress.Client, postID int) *wordpress.PostsTerm { +func getAnyOnePostsTermsTag(t *testing.T, ctx context.Context, wp *wordpress.Client, postID int) *wordpress.PostsTerm { - terms, resp, _, _ := wp.Posts().Entity(postID).Terms().Tag().List(nil) - if resp.StatusCode != http.StatusOK { + terms, resp, _ := wp.Posts.Entity(postID).Terms().Tag().List(ctx, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if len(terms) < 1 { @@ -37,8 +36,8 @@ func getAnyOnePostsTermsTag(t *testing.T, wp *wordpress.Client, postID int) *wor id := terms[0].ID - term, resp, _, _ := wp.Posts().Entity(postID).Terms().Tag().Get(id, nil) - if resp.StatusCode != http.StatusOK { + term, resp, _ := wp.Posts.Entity(postID).Terms().Tag().Get(ctx, id, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Fatalf("Expected 200 OK, got %v", resp.Status) } @@ -47,30 +46,27 @@ func getAnyOnePostsTermsTag(t *testing.T, wp *wordpress.Client, postID int) *wor func TestPostsTermsTag_InvalidCall(t *testing.T) { t.Skipf("Not supported anymore") - // User is not allowed to call create wordpress.Post object manually to retrieve PostsTermsCollection - // A proper API call would inject the right PostsTermsCollection, Client and other goodies into a post, + // User is not allowed to call create wordpress.Post object manually to retrieve PostsTermsService + // A proper API call would inject the right PostsTermsService, Client and other goodies into a post, // allowing user to call post.Terms() invalidPost := wordpress.Post{} invalidTerms := invalidPost.Terms() if invalidTerms != nil { - t.Error("Expected meta to be nil, %v", invalidTerms) + t.Errorf("Expected meta to be nil, %v", invalidTerms) } } func TestPostsTermsTagList(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() - post := getAnyOnePost(t, wp) + wp, ctx := initTestClient() + post := getAnyOnePost(t, ctx, wp) postID := post.ID - terms, resp, body, err := wp.Posts().Entity(postID).Terms().Tag().List(nil) + terms, resp, err := wp.Posts.Entity(postID).Terms().Tag().List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if terms == nil { @@ -81,19 +77,16 @@ func TestPostsTermsTagList(t *testing.T) { func TestPostsTermsTagGet(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() - post := getAnyOnePost(t, wp) + wp, ctx := initTestClient() + post := getAnyOnePost(t, ctx, wp) postID := post.ID - tt := getAnyOnePostsTermsTag(t, wp, postID) + tt := getAnyOnePostsTermsTag(t, ctx, wp, postID) - term, resp, body, err := wp.Posts().Entity(postID).Terms().Tag().Get(tt.ID, nil) + term, resp, err := wp.Posts.Entity(postID).Terms().Tag().Get(ctx, tt.ID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if term == nil { @@ -105,20 +98,17 @@ func TestPostsTermsTagGet(t *testing.T) { func TestPostsTermsTagCreate(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() - post := getAnyOnePost(t, wp) - tt := getAnyOneTermsTag(t, wp) + wp, ctx := initTestClient() + post := getAnyOnePost(t, ctx, wp) + tt := getAnyOneTermsTag(t, ctx, wp) postID := post.ID termID := tt.ID - term, resp, body, err := wp.Posts().Entity(postID).Terms().Tag().Create(termID) + term, resp, err := wp.Posts.Entity(postID).Terms().Tag().Create(ctx, termID) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusCreated { + if resp != nil && resp.StatusCode != http.StatusCreated { t.Errorf("Expected 201 Created, got %v", resp.Status) } if term == nil { @@ -132,15 +122,15 @@ func TestPostsTermsTagCreate(t *testing.T) { func TestPostsTermsTagDelete(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() - post := getAnyOnePost(t, wp) - tt := getAnyOneTermsTag(t, wp) + wp, ctx := initTestClient() + post := getAnyOnePost(t, ctx, wp) + tt := getAnyOneTermsTag(t, ctx, wp) postID := post.ID termID := tt.ID // create tag - newTerm, resp, _, _ := wp.Posts().Entity(postID).Terms().Tag().Create(termID) - if resp.StatusCode != http.StatusCreated { + newTerm, resp, _ := wp.Posts.Entity(postID).Terms().Tag().Create(ctx, termID) + if resp != nil && resp.StatusCode != http.StatusCreated { t.Errorf("Expected 201 Created, got %v", resp.Status) } if newTerm == nil { @@ -149,14 +139,11 @@ func TestPostsTermsTagDelete(t *testing.T) { // delete tag // Note: Terms does not support trashing; `force=true` is required - deletedTerm, resp, body, err := wp.Posts().Entity(postID).Terms().Tag().Delete(newTerm.ID, "force=true") + deletedTerm, resp, err := wp.Posts.Entity(postID).Terms().Tag().Delete(ctx, newTerm.ID, "force=true") if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if deletedTerm == nil { diff --git a/posts_terms_test.go b/posts_terms_test.go index 6043868..6ede655 100644 --- a/posts_terms_test.go +++ b/posts_terms_test.go @@ -7,18 +7,15 @@ import ( func TestPostsTermsList(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() - post := getAnyOnePost(t, wp) + wp, ctx := initTestClient() + post := getAnyOnePost(t, ctx, wp) postID := post.ID - terms, resp, body, err := wp.Posts().Entity(postID).Terms().List("tag", nil) + terms, resp, err := wp.Posts.Entity(postID).Terms().List(ctx, "tag", nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if terms == nil { diff --git a/posts_test.go b/posts_test.go index a3c412e..e8f6a1b 100644 --- a/posts_test.go +++ b/posts_test.go @@ -1,22 +1,24 @@ package wordpress_test import ( + "context" "fmt" - "github.com/sogko/go-wordpress" "log" "net/http" "testing" + + "github.com/robbiet480/go-wordpress" ) func factoryPost() wordpress.Post { return wordpress.Post{ - Title: wordpress.Title{ + Title: wordpress.RenderedString{ Raw: "TestPostsCreate", }, - Content: wordpress.Content{ + Content: wordpress.RenderedString{ Raw: "

HEADER

Paragraph

", }, - Excerpt: wordpress.Excerpt{ + Excerpt: wordpress.RenderedString{ Raw: "

HEADER

Paragraph

", }, Format: wordpress.PostFormatImage, @@ -29,15 +31,12 @@ func factoryPost() wordpress.Post { func cleanUpPost(t *testing.T, postID int) { - wp := initTestClient() - deletedPost, resp, body, err := wp.Posts().Delete(postID, "force=true") + wp, ctx := initTestClient() + deletedPost, resp, err := wp.Posts.Delete(ctx, postID, "force=true") if err != nil { t.Errorf("Failed to clean up new post: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if deletedPost.ID != postID { @@ -45,41 +44,38 @@ func cleanUpPost(t *testing.T, postID int) { } } -func getAnyOnePost(t *testing.T, wp *wordpress.Client) *wordpress.Post { +func getAnyOnePost(t *testing.T, ctx context.Context, wp *wordpress.Client) *wordpress.Post { - posts, resp, body, err := wp.Posts().List(nil) - if resp.StatusCode != http.StatusOK { + posts, resp, err := wp.Posts.List(ctx, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if len(posts) < 1 { log.Print(err) - log.Print(body) log.Print(resp) t.Fatalf("Should not return empty posts") } postID := posts[0].ID - post, resp, _, _ := wp.Posts().Get(postID, "context=edit") - if resp.StatusCode != http.StatusOK { + post, resp, _ := wp.Posts.Get(ctx, postID, "context=edit") + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } return post } func TestPostsList_NoParams(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - posts, resp, body, err := wp.Posts().List(nil) + posts, resp, err := wp.Posts.List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if posts == nil { t.Errorf("Should not return nil posts") } @@ -88,81 +84,71 @@ func TestPostsList_NoParams(t *testing.T) { } } func TestPostsList_WithParamsString(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() // assumes that API user authenticated with `edit_posts` - posts, resp, body, err := wp.Posts().List("filter[post_status]=draft") + posts, resp, err := wp.Posts.List(ctx, &wordpress.PostListOptions{Status: []string{"draft"}}) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if len(posts) != 0 { t.Errorf("Should return zero draft posts, returned %v", len(posts)) } - posts, resp, body, err = wp.Posts().List("filter[post_status]=publish") + posts, resp, err = wp.Posts.List(ctx, &wordpress.PostListOptions{Status: []string{"publish"}}) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if len(posts) == 0 { t.Errorf("Should return at least one published posts") } } func TestPostsGet_PostExists(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - post := getAnyOnePost(t, wp) + post := getAnyOnePost(t, ctx, wp) postID := post.ID - post, resp, body, err := wp.Posts().Get(postID, nil) + post, resp, err := wp.Posts.Get(ctx, postID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("body should not be nil") - } if post.ID != postID { t.Errorf("Returned post should have the same ID as specified in Get(), %v != %v", post.ID, postID) } } func TestPostsGet_PostDoesNotExists(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() postID := -1 - _, resp, body, err := wp.Posts().Get(postID, nil) + _, resp, err := wp.Posts.Get(ctx, postID, nil) if err == nil { t.Errorf("Should return error") } - if resp.StatusCode != http.StatusNotFound { + if resp != nil && resp.StatusCode != http.StatusNotFound { t.Errorf("Expected 400 NotFound, got %v", resp.Status) } - if body == nil { - t.Errorf("body should not be nil") - } } func TestPostsGet_Lazy(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - post := getAnyOnePost(t, wp) + post := getAnyOnePost(t, ctx, wp) postID := post.ID - //The proper way to get lazy-fetch posts. Posts().Entity() won't make any HTTP request - lazyPost := wp.Posts().Entity(postID) + //The proper way to get lazy-fetch posts. Posts.Entity() won't make any HTTP request + lazyPost := wp.Posts.Entity(postID) if lazyPost == nil { t.Errorf("lazyPost should not be nil") } @@ -174,16 +160,13 @@ func TestPostsGet_Lazy(t *testing.T) { } // populate Post Entity - post, resp, body, err := lazyPost.Populate(nil) + post, resp, err := lazyPost.Populate(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("body should not be nil") - } if post.ID != postID { t.Errorf("Returned post should have the same ID as specified in Get(), %v != %v", post.ID, postID) } @@ -193,19 +176,16 @@ func TestPostsGet_Lazy(t *testing.T) { } func TestPostsCreate(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() p := factoryPost() - newPost, resp, body, err := wp.Posts().Create(&p) + newPost, resp, err := wp.Posts.Create(ctx, &p) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusCreated { + if resp != nil && resp.StatusCode != http.StatusCreated { t.Errorf("Expected 201 Created, got %v", resp.Status) } - if body == nil { - t.Errorf("body should not be nil") - } if newPost == nil { t.Errorf("newPost should not be nil") } @@ -230,18 +210,18 @@ func TestPostsCreate(t *testing.T) { } func TestPostsUpdate(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() // create a new post first p := factoryPost() - newPost, resp, _, _ := wp.Posts().Create(&p) - if resp.StatusCode != http.StatusCreated { + newPost, resp, _ := wp.Posts.Create(ctx, &p) + if resp != nil && resp.StatusCode != http.StatusCreated { t.Fatalf("Expected 201 Created, got %v", resp.Status) } // get the post in `edit` context - post, resp, _, _ := wp.Posts().Get(newPost.ID, "context=edit") - if resp.StatusCode != http.StatusOK { + post, resp, _ := wp.Posts.Get(ctx, newPost.ID, "context=edit") + if resp != nil && resp.StatusCode != http.StatusOK { t.Fatalf("Expected 200 OK, got %v", resp.Status) } @@ -253,16 +233,13 @@ func TestPostsUpdate(t *testing.T) { post.Title.Raw = newTitle // update post - updatePost, resp, body, err := wp.Posts().Update(post.ID, post) + updatePost, resp, err := wp.Posts.Update(ctx, post.ID, post) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("body should not be nil") - } if updatePost == nil { t.Errorf("updatePost should not be nil") } @@ -275,26 +252,23 @@ func TestPostsUpdate(t *testing.T) { } func TestPostsDelete_NoParams_MoveToTrash(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() // create a new post first p := factoryPost() - newPost, resp, _, _ := wp.Posts().Create(&p) - if resp.StatusCode != http.StatusCreated { + newPost, resp, _ := wp.Posts.Create(ctx, &p) + if resp != nil && resp.StatusCode != http.StatusCreated { t.Errorf("Expected 201 Created, got %v", resp.Status) } // delete post (move to trash) - deletedPost, resp, body, err := wp.Posts().Delete(newPost.ID, nil) + deletedPost, resp, err := wp.Posts.Delete(ctx, newPost.ID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("body should not be nil") - } if deletedPost == nil { t.Errorf("updatePost should not be nil") } @@ -306,26 +280,23 @@ func TestPostsDelete_NoParams_MoveToTrash(t *testing.T) { cleanUpPost(t, newPost.ID) } func TestPostsDelete_WithParams_DeletePermanently(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() // create a new post first p := factoryPost() - newPost, resp, _, _ := wp.Posts().Create(&p) - if resp.StatusCode != http.StatusCreated { + newPost, resp, _ := wp.Posts.Create(ctx, &p) + if resp != nil && resp.StatusCode != http.StatusCreated { t.Errorf("Expected 201 Created, got %v", resp.Status) } // delete post (delete permanently) - deletedPost, resp, body, err := wp.Posts().Delete(newPost.ID, "force=true") + deletedPost, resp, err := wp.Posts.Delete(ctx, newPost.ID, "force=true") if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } - if body == nil { - t.Errorf("body should not be nil") - } if deletedPost == nil { t.Errorf("updatePost should not be nil") } diff --git a/revisions.go b/revisions.go index 4900335..65110e4 100644 --- a/revisions.go +++ b/revisions.go @@ -1,49 +1,53 @@ package wordpress import ( + "context" "fmt" - "net/http" ) +// Revision represents a WordPress page/post revision. type Revision struct { - ID int `json:"id,omitempty"` - Author string `json:"author,omitempty"` // TODO: File a WP-API bug, why am I getting string instead of int? - Date string `json:"date,omitempty"` - DateGMT string `json:"dateGMT,omitempty"` - GUID string `json:"guid,omitempty"` - Modified string `json:"modified,omitempty"` - ModifiedGMT string `json:"modifiedGMT,omitempty"` - Parent int `json:"parent,omitempty"` - Slug string `json:"slug,omitempty"` - Title string `json:"title,omitempty"` - Content string `json:"content,omitempty"` - Excerpt string `json:"excerpt,omitempty"` + ID int `json:"id,omitempty"` + Author int `json:"author,omitempty"` + Date Time `json:"date,omitempty"` + DateGMT Time `json:"date_gmt,omitempty"` + GUID RenderedString `json:"guid,omitempty"` + Modified Time `json:"modified,omitempty"` + ModifiedGMT Time `json:"modified_gmt,omitempty"` + Parent int `json:"parent,omitempty"` + Slug string `json:"slug,omitempty"` + Title RenderedString `json:"title,omitempty"` + Content RenderedString `json:"content,omitempty"` + Excerpt RenderedString `json:"excerpt,omitempty"` } -type RevisionsCollection struct { - client *Client +// RevisionsService provides access to the revision related functions in the WordPress REST API. +type RevisionsService struct { + service url string parent interface{} parentType string } -func (col *RevisionsCollection) List(params interface{}) ([]Revision, *http.Response, []byte, error) { - var revisions []Revision - resp, body, err := col.client.List(col.url, params, &revisions) - return revisions, resp, body, err +// List returns a list of revisions. +func (c *RevisionsService) List(ctx context.Context, params interface{}) ([]*Revision, *Response, error) { + var revisions []*Revision + resp, err := c.client.List(ctx, c.url, params, &revisions) + return revisions, resp, err } -func (col *RevisionsCollection) Get(id int, params interface{}) (*Revision, *http.Response, []byte, error) { +// Get returns a single revision for the given id. +func (c *RevisionsService) Get(ctx context.Context, id int, params interface{}) (*Revision, *Response, error) { var revision Revision - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Get(entityURL, params, &revision) - return &revision, resp, body, err + entityURL := fmt.Sprintf("%v/%v", c.url, id) + resp, err := c.client.Get(ctx, entityURL, params, &revision) + return &revision, resp, err } -// TODO: file an issue for inconsistent response -func (col *RevisionsCollection) Delete(id int, params interface{}) (bool, *http.Response, []byte, error) { - var response bool - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Delete(entityURL, "force=true", &response) - return response, resp, body, err +// Delete removes the revision with the given id. +func (c *RevisionsService) Delete(ctx context.Context, id int, params interface{}) (*Revision, *Response, error) { + var response Revision + entityURL := fmt.Sprintf("%v/%v", c.url, id) + resp, err := c.client.Delete(ctx, entityURL, "force=true", &response) + return &response, resp, err } diff --git a/settings.go b/settings.go new file mode 100644 index 0000000..377f4c5 --- /dev/null +++ b/settings.go @@ -0,0 +1,32 @@ +package wordpress + +import "context" + +// Settings represents a WordPress settings. +type Settings struct { + Title string `json:"title"` + Description string `json:"description"` + URL string `json:"url"` + Email string `json:"email"` + Timezone string `json:"timezone"` + DateFormat string `json:"date_format"` + TimeFormat string `json:"time_format"` + StartOfWeek int `json:"start_of_week"` + Language string `json:"language"` + UseSmilies bool `json:"use_smilies"` + DefaultCategory int `json:"default_category"` + DefaultPostFormat string `json:"default_post_format"` + PostsPerPage int `json:"posts_per_page"` + DefaultPingStatus string `json:"default_ping_status"` + DefaultCommentStatus string `json:"default_comment_status"` +} + +// SettingsService provides access to the settings related functions in the WordPress REST API. +type SettingsService service + +// List returns a list of settingss. +func (c *SettingsService) List(ctx context.Context) (*Settings, *Response, error) { + var settings Settings + resp, err := c.client.List(ctx, "settings", nil, &settings) + return &settings, resp, err +} diff --git a/settings_test.go b/settings_test.go new file mode 100644 index 0000000..393dde4 --- /dev/null +++ b/settings_test.go @@ -0,0 +1,21 @@ +package wordpress_test + +import ( + "net/http" + "testing" +) + +func TestSettingsList(t *testing.T) { + client, ctx := initTestClient() + + settings, resp, err := client.Settings.List(ctx) + if err != nil { + t.Errorf("Should not return error: %v", err.Error()) + } + if resp != nil && resp.StatusCode != http.StatusOK { + t.Errorf("Expected 200 StatusOK, got %v", resp.Status) + } + if settings == nil { + t.Errorf("Should not return nil settings") + } +} diff --git a/statuses.go b/statuses.go index 59a17e1..1a28b5a 100644 --- a/statuses.go +++ b/statuses.go @@ -1,10 +1,11 @@ package wordpress import ( + "context" "fmt" - "net/http" ) +// Status represents a WordPress post status. type Status struct { Name string `json:"name,omitempty"` Private bool `json:"private,omitempty"` @@ -14,6 +15,7 @@ type Status struct { Slug string `json:"slug,omitempty"` } +// Statuses describes multiple Statuses. type Statuses struct { Publish Status `json:"publish,omitempty"` Future Status `json:"future,omitempty"` @@ -21,20 +23,21 @@ type Statuses struct { Pending Status `json:"pending,omitempty"` Private Status `json:"private,omitempty"` } -type StatusesCollection struct { - client *Client - url string -} -func (col *StatusesCollection) List(params interface{}) (*Statuses, *http.Response, []byte, error) { +// StatusesService provides access to the Status related functions in the WordPress REST API. +type StatusesService service + +// List returns a list of statuses. +func (c *StatusesService) List(ctx context.Context, params interface{}) (*Statuses, *Response, error) { var statuses Statuses - resp, body, err := col.client.List(col.url, params, &statuses) - return &statuses, resp, body, err + resp, err := c.client.List(ctx, "statuses", params, &statuses) + return &statuses, resp, err } -func (col *StatusesCollection) Get(slug string, params interface{}) (*Status, *http.Response, []byte, error) { +// Get returns a single status for the given id. +func (c *StatusesService) Get(ctx context.Context, slug string, params interface{}) (*Status, *Response, error) { var entity Status - entityURL := fmt.Sprintf("%v/%v", col.url, slug) - resp, body, err := col.client.Get(entityURL, params, &entity) - return &entity, resp, body, err + entityURL := fmt.Sprintf("statuses/%v", slug) + resp, err := c.client.Get(ctx, entityURL, params, &entity) + return &entity, resp, err } diff --git a/statuses_test.go b/statuses_test.go index bc16578..8f3fd61 100644 --- a/statuses_test.go +++ b/statuses_test.go @@ -6,36 +6,32 @@ import ( ) func TestStatusesList(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - statuses, resp, body, err := wp.Statuses().List(nil) + statuses, resp, err := wp.Statuses.List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if statuses == nil { t.Errorf("Should not return nil statuses") } } func TestStatusesGet(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - status, resp, body, err := wp.Statuses().Get("publish", nil) + status, resp, err := wp.Statuses.Get(ctx, "publish", nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if status == nil { t.Errorf("Should not return nil status") } diff --git a/tags.go b/tags.go new file mode 100644 index 0000000..b8f44b7 --- /dev/null +++ b/tags.go @@ -0,0 +1,71 @@ +package wordpress + +import ( + "context" + "fmt" +) + +// Tag represents a WordPress page/post tag. +type Tag struct { + ID int `json:"id,omitempty"` + Count int `json:"count,omitempty"` + Description string `json:"description,omitempty"` + Link string `json:"link,omitempty"` + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` + Taxonomy string `json:"taxonomy,omitempty"` +} + +// TagsService provides access to the Tag related functions in the WordPress REST API. +type TagsService service + +// List returns a list of tags. +func (c *TagsService) List(ctx context.Context, opts *TagListOptions) ([]*Tag, *Response, error) { + u, err := addOptions("tags", opts) + if err != nil { + return nil, nil, err + } + + req, err := c.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + tags := []*Tag{} + resp, err := c.client.Do(ctx, req, &tags) + if err != nil { + return nil, resp, err + } + return tags, resp, nil +} + +// Create creates a new tag. +func (c *TagsService) Create(ctx context.Context, newTag *Tag) (*Tag, *Response, error) { + var created Tag + resp, err := c.client.Create(ctx, "tags", newTag, &created) + return &created, resp, err +} + +// Get returns a single tag for the given id. +func (c *TagsService) Get(ctx context.Context, id int, params interface{}) (*Tag, *Response, error) { + var entity Tag + entityURL := fmt.Sprintf("tags/%v", id) + resp, err := c.client.Get(ctx, entityURL, params, &entity) + return &entity, resp, err +} + +// Update updates a single tag with the given id. +func (c *TagsService) Update(ctx context.Context, id int, post *Tag) (*Tag, *Response, error) { + var updated Tag + entityURL := fmt.Sprintf("tags/%v", id) + resp, err := c.client.Update(ctx, entityURL, post, &updated) + return &updated, resp, err +} + +// Delete removes the tag with the given id. +func (c *TagsService) Delete(ctx context.Context, id int, params interface{}) (*Tag, *Response, error) { + var deleted Tag + entityURL := fmt.Sprintf("tags/%v", id) + resp, err := c.client.Delete(ctx, entityURL, params, &deleted) + return &deleted, resp, err +} diff --git a/taxonomies.go b/taxonomies.go index e398a69..c6ec147 100644 --- a/taxonomies.go +++ b/taxonomies.go @@ -1,33 +1,35 @@ package wordpress import ( + "context" "fmt" - "net/http" ) +// Taxonomy represents a WordPress taxonomy. type Taxonomy struct { Description string `json:"description,omitempty"` Hierarchical bool `json:"hierarchical,omitempty"` Labels map[string]interface{} `json:"labels,omitempty"` Name string `json:"name,omitempty"` - Slug string `json:"slug,omitempty"` ShowCloud bool `json:"show_cloud,omitempty"` + Slug string `json:"slug,omitempty"` Types []string `json:"types,omitempty"` } -type TaxonomiesCollection struct { - client *Client - url string -} -func (col *TaxonomiesCollection) List(params interface{}) (map[string]Taxonomy, *http.Response, []byte, error) { +// TaxonomiesService provides access to the Taxonomies related functions in the WordPress REST API. +type TaxonomiesService service + +// List returns a list of taxonomies. +func (c *TaxonomiesService) List(ctx context.Context, params interface{}) (map[string]Taxonomy, *Response, error) { var taxonomies map[string]Taxonomy - resp, body, err := col.client.List(col.url, params, &taxonomies) - return taxonomies, resp, body, err + resp, err := c.client.List(ctx, "taxonomies", params, &taxonomies) + return taxonomies, resp, err } -func (col *TaxonomiesCollection) Get(slug string, params interface{}) (*Taxonomy, *http.Response, []byte, error) { +// Get returns a single taxonomy for the given id. +func (c *TaxonomiesService) Get(ctx context.Context, slug string, params interface{}) (*Taxonomy, *Response, error) { var taxonomy Taxonomy - entityURL := fmt.Sprintf("%v/%v", col.url, slug) - resp, body, err := col.client.Get(entityURL, params, &taxonomy) - return &taxonomy, resp, body, err + entityURL := fmt.Sprintf("taxonomies/%v", slug) + resp, err := c.client.Get(ctx, entityURL, params, &taxonomy) + return &taxonomy, resp, err } diff --git a/taxonomies_test.go b/taxonomies_test.go index 0f57265..33a2935 100644 --- a/taxonomies_test.go +++ b/taxonomies_test.go @@ -6,18 +6,16 @@ import ( ) func TestTaxonomiesList(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - taxonomies, resp, body, err := wp.Taxonomies().List(nil) + taxonomies, resp, err := wp.Taxonomies.List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if taxonomies == nil { t.Errorf("Should not return nil taxonomies") } @@ -27,36 +25,32 @@ func TestTaxonomiesList(t *testing.T) { } func TestTaxonomiesGet_TaxonomyExists(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - taxonomy, resp, body, err := wp.Taxonomies().Get("post_tag", nil) + taxonomy, resp, err := wp.Taxonomies.Get(ctx, "post_tag", nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if taxonomy == nil { t.Errorf("Should not return nil taxonomies") } } func TestTaxonomiesGet_TaxonomyDoesNotExists(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - taxonomy, resp, body, err := wp.Taxonomies().Get("RANDOM", nil) + taxonomy, resp, err := wp.Taxonomies.Get(ctx, "RANDOM", nil) if err == nil { t.Errorf("Should return error") } - if resp.StatusCode != http.StatusNotFound { + if resp != nil && resp.StatusCode != http.StatusNotFound { t.Errorf("Expected 404 Not Found, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if taxonomy == nil { t.Errorf("Should not return nil taxonomies") } diff --git a/terms.go b/terms.go index 9c0e343..facabc1 100644 --- a/terms.go +++ b/terms.go @@ -1,10 +1,11 @@ package wordpress import ( + "context" "fmt" - "net/http" ) +// Term represents a WordPress page/post term. type Term struct { ID int `json:"id,omitempty"` Count int `json:"integer,omitempty"` @@ -15,63 +16,77 @@ type Term struct { Taxonomy string `json:"taxonomy,omitempty"` Parent int `json:"parent,omitempty"` } -type TermsCollection struct { - client *Client - url string -} -func (col *TermsCollection) List(taxonomy string, params interface{}) ([]Term, *http.Response, []byte, error) { - var terms []Term - url := fmt.Sprintf("%v/%v", col.url, taxonomy) - resp, body, err := col.client.List(url, params, &terms) - return terms, resp, body, err +// TermsService provides access to the Terms related functions in the WordPress REST API. +type TermsService service + +// List returns a list of terms. +func (c *TermsService) List(ctx context.Context, taxonomy string, params interface{}) ([]*Term, *Response, error) { + var terms []*Term + url := fmt.Sprintf("terms/%v", taxonomy) + resp, err := c.client.List(ctx, url, params, &terms) + return terms, resp, err } -func (col *TermsCollection) Tag() *TermsTaxonomyCollection { - return &TermsTaxonomyCollection{ - client: col.client, - url: fmt.Sprintf("%v/tag", col.url), + +// Tag returns the terms taxonomy service configured for tags. +func (c *TermsService) Tag() *TermsTaxonomyService { + return &TermsTaxonomyService{ + client: c.client, + url: fmt.Sprintf("%v/tag", "terms"), taxonomyBase: "tag", } } -func (col *TermsCollection) Category() *TermsTaxonomyCollection { - return &TermsTaxonomyCollection{ - client: col.client, - url: fmt.Sprintf("%v/category", col.url), + +// Category returns the terms taxonomy service configured for categories. +func (c *TermsService) Category() *TermsTaxonomyService { + return &TermsTaxonomyService{ + client: c.client, + url: fmt.Sprintf("%v/category", "terms"), taxonomyBase: "category", } } -type TermsTaxonomyCollection struct { +// TermsTaxonomyService contains information about a taxonomy term. +type TermsTaxonomyService struct { client *Client url string taxonomyBase string } -func (col *TermsTaxonomyCollection) List(params interface{}) ([]Term, *http.Response, []byte, error) { - var terms []Term - resp, body, err := col.client.List(col.url, params, &terms) - return terms, resp, body, err +// List returns a list of terms. +func (c *TermsTaxonomyService) List(ctx context.Context, params interface{}) ([]*Term, *Response, error) { + var terms []*Term + resp, err := c.client.List(ctx, c.url, params, &terms) + return terms, resp, err } -func (col *TermsTaxonomyCollection) Create(new *Term) (*Term, *http.Response, []byte, error) { + +// Create creates a new term. +func (c *TermsTaxonomyService) Create(ctx context.Context, newTerm *Term) (*Term, *Response, error) { var created Term - resp, body, err := col.client.Create(col.url, new, &created) - return &created, resp, body, err + resp, err := c.client.Create(ctx, c.url, newTerm, &created) + return &created, resp, err } -func (col *TermsTaxonomyCollection) Get(id int, params interface{}) (*Term, *http.Response, []byte, error) { + +// Get returns a single term for the given id. +func (c *TermsTaxonomyService) Get(ctx context.Context, id int, params interface{}) (*Term, *Response, error) { var entity Term - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Get(entityURL, params, &entity) - return &entity, resp, body, err + entityURL := fmt.Sprintf("%v/%v", c.url, id) + resp, err := c.client.Get(ctx, entityURL, params, &entity) + return &entity, resp, err } -func (col *TermsTaxonomyCollection) Update(id int, post *Term) (*Term, *http.Response, []byte, error) { + +// Update updates a single term with the given id. +func (c *TermsTaxonomyService) Update(ctx context.Context, id int, post *Term) (*Term, *Response, error) { var updated Term - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Update(entityURL, post, &updated) - return &updated, resp, body, err + entityURL := fmt.Sprintf("%v/%v", c.url, id) + resp, err := c.client.Update(ctx, entityURL, post, &updated) + return &updated, resp, err } -func (col *TermsTaxonomyCollection) Delete(id int, params interface{}) (*Term, *http.Response, []byte, error) { + +// Delete removes the term with the given id. +func (c *TermsTaxonomyService) Delete(ctx context.Context, id int, params interface{}) (*Term, *Response, error) { var deleted Term - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Delete(entityURL, params, &deleted) - return &deleted, resp, body, err + entityURL := fmt.Sprintf("%v/%v", c.url, id) + resp, err := c.client.Delete(ctx, entityURL, params, &deleted) + return &deleted, resp, err } diff --git a/terms_category_test.go b/terms_category_test.go index 4fa66eb..fb42269 100644 --- a/terms_category_test.go +++ b/terms_category_test.go @@ -1,10 +1,11 @@ package wordpress_test import ( - "github.com/sogko/go-wordpress" - "log" + "context" "net/http" "testing" + + "github.com/robbiet480/go-wordpress" ) func factoryTermsCategory() *wordpress.Term { @@ -16,15 +17,12 @@ func factoryTermsCategory() *wordpress.Term { func cleanUpTermsCategory(t *testing.T, id int) { - wp := initTestClient() - deletedTerm, resp, body, err := wp.Terms().Category().Delete(id, nil) + wp, ctx := initTestClient() + deletedTerm, resp, err := wp.Terms.Category().Delete(ctx, id, nil) if err != nil { t.Errorf("Failed to clean up new term: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if deletedTerm.ID != id { @@ -32,10 +30,10 @@ func cleanUpTermsCategory(t *testing.T, id int) { } } -func getAnyOneTermsCategory(t *testing.T, wp *wordpress.Client) *wordpress.Term { +func getAnyOneTermsCategory(t *testing.T, ctx context.Context, wp *wordpress.Client) *wordpress.Term { - terms, resp, _, _ := wp.Terms().Category().List(nil) - if resp.StatusCode != http.StatusOK { + terms, resp, _ := wp.Terms.Category().List(ctx, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if len(terms) < 1 { @@ -44,8 +42,8 @@ func getAnyOneTermsCategory(t *testing.T, wp *wordpress.Client) *wordpress.Term id := terms[0].ID - term, resp, _, _ := wp.Terms().Category().Get(id, nil) - if resp.StatusCode != http.StatusOK { + term, resp, _ := wp.Terms.Category().Get(ctx, id, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Fatalf("Expected 200 OK, got %v", resp.Status) } @@ -54,16 +52,13 @@ func getAnyOneTermsCategory(t *testing.T, wp *wordpress.Client) *wordpress.Term func TestTermsCategoryList(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() + wp, ctx := initTestClient() - terms, resp, body, err := wp.Terms().Category().List(nil) + terms, resp, err := wp.Terms.Category().List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if terms == nil { @@ -74,17 +69,14 @@ func TestTermsCategoryList(t *testing.T) { func TestTermsCategoryGet(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() - tt := getAnyOneTermsCategory(t, wp) + wp, ctx := initTestClient() + tt := getAnyOneTermsCategory(t, ctx, wp) - term, resp, body, err := wp.Terms().Category().Get(tt.ID, nil) + term, resp, err := wp.Terms.Category().Get(ctx, tt.ID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if term == nil { @@ -96,19 +88,17 @@ func TestTermsCategoryGet(t *testing.T) { func TestTermsCategoryCreate_New(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() + wp, ctx := initTestClient() tt := factoryTermsCategory() - term, resp, body, err := wp.Terms().Category().Create(tt) + term, resp, err := wp.Terms.Category().Create(ctx, tt) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) + } if term == nil { t.Errorf("Should not return nil term") @@ -121,19 +111,16 @@ func TestTermsCategoryCreate_New(t *testing.T) { func TestTermsCategoryCreate_Existing(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() + wp, ctx := initTestClient() tt := factoryTermsCategory() // add category the first time - term, resp, body, err := wp.Terms().Category().Create(tt) + term, resp, err := wp.Terms.Category().Create(ctx, tt) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if term == nil { @@ -141,34 +128,31 @@ func TestTermsCategoryCreate_Existing(t *testing.T) { } // add the same category the second time - duplicateTerm, resp, body, err := wp.Terms().Category().Create(tt) + duplicateTerm, resp, err := wp.Terms.Category().Create(ctx, tt) if err == nil { t.Errorf("Should return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusInternalServerError { + if resp != nil && resp.StatusCode != http.StatusInternalServerError { t.Errorf("Expected 500 Internal Server Erro, got %v", resp.Status) } if duplicateTerm == nil { t.Errorf("Should not return nil duplicateTerm") } - // unmarshall error response - // We expect server to return "term_exists" error code - serverErrors, err := wordpress.UnmarshallServerError(body) - if err != nil { - cleanUpTermsCategory(t, term.ID) - log.Println(string(body)) - t.Fatalf("Unexpected error response from server, unable to unmarshall message %v", err.Error()) - } - if len(serverErrors) != 1 { - t.Errorf("Expected one error", len(serverErrors)) - } - if serverErrors[0].Code != "term_exists" { - t.Errorf("Unexpected err.code, %v != term_exists", serverErrors[0].Code) - } + // // unmarshall error response + // // We expect server to return "term_exists" error code + // serverErrors, err := wordpress.UnmarshalServerError(body) + // if err != nil { + // cleanUpTermsCategory(t, term.ID) + // log.Println(string(body)) + // t.Fatalf("Unexpected error response from server, unable to unmarshall message %v", err.Error()) + // } + // if len(serverErrors) != 1 { + // t.Error("Expected one error", len(serverErrors)) + // } + // if serverErrors[0].Code != "term_exists" { + // t.Errorf("Unexpected err.code, %v != term_exists", serverErrors[0].Code) + // } // clean up cleanUpTermsCategory(t, term.ID) @@ -178,13 +162,13 @@ func TestTermsCategoryCreate_Existing(t *testing.T) { func TestTermsCategoryDelete(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() + wp, ctx := initTestClient() tt := factoryTermsCategory() // create category - newTerm, resp, _, _ := wp.Terms().Category().Create(tt) - if resp.StatusCode != http.StatusOK { + newTerm, resp, _ := wp.Terms.Category().Create(ctx, tt) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if newTerm == nil { @@ -192,14 +176,11 @@ func TestTermsCategoryDelete(t *testing.T) { } // delete category - deletedTerm, resp, body, err := wp.Terms().Category().Delete(newTerm.ID, nil) + deletedTerm, resp, err := wp.Terms.Category().Delete(ctx, newTerm.ID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if deletedTerm == nil { @@ -210,13 +191,13 @@ func TestTermsCategoryDelete(t *testing.T) { func TestTermsCategoryUpdate(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() + wp, ctx := initTestClient() tt := factoryTermsCategory() // create category - newTerm, resp, _, _ := wp.Terms().Category().Create(tt) - if resp.StatusCode != http.StatusOK { + newTerm, resp, _ := wp.Terms.Category().Create(ctx, tt) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if newTerm == nil { @@ -224,8 +205,8 @@ func TestTermsCategoryUpdate(t *testing.T) { } // get category term - term, resp, _, _ := wp.Terms().Category().Get(newTerm.ID, nil) - if resp.StatusCode != http.StatusOK { + term, resp, _ := wp.Terms.Category().Get(ctx, newTerm.ID, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if term == nil { @@ -240,14 +221,11 @@ func TestTermsCategoryUpdate(t *testing.T) { term.Description = newTermDescription // update - updatedTerm, resp, body, err := wp.Terms().Category().Update(newTerm.ID, term) + updatedTerm, resp, err := wp.Terms.Category().Update(ctx, newTerm.ID, term) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if updatedTerm == nil { diff --git a/terms_tag_test.go b/terms_tag_test.go index 239cae8..e6b6182 100644 --- a/terms_tag_test.go +++ b/terms_tag_test.go @@ -1,9 +1,11 @@ package wordpress_test import ( - "github.com/sogko/go-wordpress" + "context" "net/http" "testing" + + "github.com/robbiet480/go-wordpress" ) func factoryTermsTag() *wordpress.Term { @@ -15,15 +17,12 @@ func factoryTermsTag() *wordpress.Term { func cleanUpTermsTag(t *testing.T, id int) { - wp := initTestClient() - deletedTerm, resp, body, err := wp.Terms().Tag().Delete(id, nil) + wp, ctx := initTestClient() + deletedTerm, resp, err := wp.Terms.Tag().Delete(ctx, id, nil) if err != nil { t.Errorf("Failed to clean up new term: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if deletedTerm.ID != id { @@ -31,10 +30,10 @@ func cleanUpTermsTag(t *testing.T, id int) { } } -func getAnyOneTermsTag(t *testing.T, wp *wordpress.Client) *wordpress.Term { +func getAnyOneTermsTag(t *testing.T, ctx context.Context, wp *wordpress.Client) *wordpress.Term { - terms, resp, _, _ := wp.Terms().Tag().List(nil) - if resp.StatusCode != http.StatusOK { + terms, resp, _ := wp.Terms.Tag().List(ctx, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if len(terms) < 1 { @@ -43,8 +42,8 @@ func getAnyOneTermsTag(t *testing.T, wp *wordpress.Client) *wordpress.Term { id := terms[0].ID - term, resp, _, _ := wp.Terms().Tag().Get(id, nil) - if resp.StatusCode != http.StatusOK { + term, resp, _ := wp.Terms.Tag().Get(ctx, id, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Fatalf("Expected 200 OK, got %v", resp.Status) } @@ -53,16 +52,13 @@ func getAnyOneTermsTag(t *testing.T, wp *wordpress.Client) *wordpress.Term { func TestTermsTagList(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() + wp, ctx := initTestClient() - terms, resp, body, err := wp.Terms().Tag().List(nil) + terms, resp, err := wp.Terms.Tag().List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if terms == nil { @@ -73,17 +69,14 @@ func TestTermsTagList(t *testing.T) { func TestTermsTagGet(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() - tt := getAnyOneTermsTag(t, wp) + wp, ctx := initTestClient() + tt := getAnyOneTermsTag(t, ctx, wp) - term, resp, body, err := wp.Terms().Tag().Get(tt.ID, nil) + term, resp, err := wp.Terms.Tag().Get(ctx, tt.ID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if term == nil { @@ -95,18 +88,15 @@ func TestTermsTagGet(t *testing.T) { func TestTermsTagCreate(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() + wp, ctx := initTestClient() tt := factoryTermsTag() - term, resp, body, err := wp.Terms().Tag().Create(tt) + term, resp, err := wp.Terms.Tag().Create(ctx, tt) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if term == nil { @@ -120,13 +110,13 @@ func TestTermsTagCreate(t *testing.T) { func TestTermsTagDelete(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() + wp, ctx := initTestClient() tt := factoryTermsTag() // create tag - newTerm, resp, _, _ := wp.Terms().Tag().Create(tt) - if resp.StatusCode != http.StatusOK { + newTerm, resp, _ := wp.Terms.Tag().Create(ctx, tt) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if newTerm == nil { @@ -134,14 +124,11 @@ func TestTermsTagDelete(t *testing.T) { } // delete tag - deletedTerm, resp, body, err := wp.Terms().Tag().Delete(newTerm.ID, nil) + deletedTerm, resp, err := wp.Terms.Tag().Delete(ctx, newTerm.ID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if deletedTerm == nil { @@ -152,13 +139,13 @@ func TestTermsTagDelete(t *testing.T) { func TestTermsTagUpdate(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() + wp, ctx := initTestClient() tt := factoryTermsTag() // create tag - newTerm, resp, _, _ := wp.Terms().Tag().Create(tt) - if resp.StatusCode != http.StatusOK { + newTerm, resp, _ := wp.Terms.Tag().Create(ctx, tt) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if newTerm == nil { @@ -166,8 +153,8 @@ func TestTermsTagUpdate(t *testing.T) { } // get tag term - term, resp, _, _ := wp.Terms().Tag().Get(newTerm.ID, nil) - if resp.StatusCode != http.StatusOK { + term, resp, _ := wp.Terms.Tag().Get(ctx, newTerm.ID, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if term == nil { @@ -182,14 +169,11 @@ func TestTermsTagUpdate(t *testing.T) { term.Description = newTermDescription // update - updatedTerm, resp, body, err := wp.Terms().Tag().Update(newTerm.ID, term) + updatedTerm, resp, err := wp.Terms.Tag().Update(ctx, newTerm.ID, term) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if updatedTerm == nil { diff --git a/terms_test.go b/terms_test.go index b3de65c..714076e 100644 --- a/terms_test.go +++ b/terms_test.go @@ -7,16 +7,13 @@ import ( func TestTermsList(t *testing.T) { t.Skipf("Not supported anymore") - wp := initTestClient() + wp, ctx := initTestClient() - terms, resp, body, err := wp.Terms().List("tag", nil) + terms, resp, err := wp.Terms.List(ctx, "tag", nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if terms == nil { diff --git a/time.go b/time.go new file mode 100644 index 0000000..3dcb082 --- /dev/null +++ b/time.go @@ -0,0 +1,42 @@ +package wordpress + +import ( + "fmt" + "time" +) + +// Location is the time.Location used when decoding timestamps from WordPress. +var Location = time.UTC + +// Time is a wrapper around time.Time with custom JSON marshal/unmarshal functions for the WordPress specific timestamp formats. +type Time struct { + time.Time +} + +// TimeLayout is the layout string for a timestamp without timezone information like 2017-12-25T09:54:42 +const TimeLayout = "2006-01-02T15:04:05" + +// TimeWithZoneLayout is the layout string for a timestamp with timezone information like 2017-09-24T13:28:06+00:00. +const TimeWithZoneLayout = "2006-01-02T15:04:05-07:00" + +// UnmarshalJSON unmarshals the timestamp with one of the WordPress specific formats. +func (t *Time) UnmarshalJSON(b []byte) error { + if b[0] == '"' && b[len(b)-1] == '"' { + b = b[1 : len(b)-1] + } + zoneTime, err := time.Parse(TimeWithZoneLayout, string(b)) + if err != nil { + noZoneTime, altErr := time.ParseInLocation(TimeLayout, string(b), Location) + if altErr != nil { + return err + } + zoneTime = noZoneTime + } + t.Time = zoneTime + return nil +} + +// MarshalJSON returns a WordPress formatted timestamp. +func (t *Time) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%s"`, t.Time.Format(TimeLayout))), nil +} diff --git a/transports.go b/transports.go new file mode 100644 index 0000000..0f325c3 --- /dev/null +++ b/transports.go @@ -0,0 +1,46 @@ +package wordpress + +import "net/http" + +// BasicAuthTransport is an http.RoundTripper that authenticates all requests +// using HTTP Basic Authentication with the provided username and password. +type BasicAuthTransport struct { + Username string // WordPress username + Password string // WordPress password + + // Transport is the underlying HTTP transport to use when making requests. + // It will default to http.DefaultTransport if nil. + Transport http.RoundTripper +} + +// RoundTrip implements the RoundTripper interface. +func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // To set extra headers, we must make a copy of the Request so + // that we don't modify the Request we were given. This is required by the + // specification of http.RoundTripper. + // + // Since we are going to modify only req.Header here, we only need a deep copy + // of req.Header. + req2 := new(http.Request) + *req2 = *req + req2.Header = make(http.Header, len(req.Header)) + for k, s := range req.Header { + req2.Header[k] = append([]string(nil), s...) + } + + req2.SetBasicAuth(t.Username, t.Password) + return t.transport().RoundTrip(req2) +} + +// Client returns an *http.Client that makes requests that are authenticated +// using HTTP Basic Authentication. +func (t *BasicAuthTransport) Client() *http.Client { + return &http.Client{Jar: nil, Transport: t} +} + +func (t *BasicAuthTransport) transport() http.RoundTripper { + if t.Transport != nil { + return t.Transport + } + return http.DefaultTransport +} diff --git a/types.go b/types.go index aa628db..597fd0f 100644 --- a/types.go +++ b/types.go @@ -1,10 +1,11 @@ package wordpress import ( + "context" "fmt" - "net/http" ) +// TypeLabels represents a label that applies to a WordPress Type. type TypeLabels struct { Name string `json:"name,omitempty"` SingularName string `json:"singular_name,omitempty"` @@ -21,6 +22,8 @@ type TypeLabels struct { MenuName string `json:"menu_name,omitempty"` NameAdminBar string `json:"name_admin_bar,omitempty"` } + +// Type represents a WordPress item type. type Type struct { Description string `json:"description,omitempty"` Hierarchical bool `json:"hierarchical,omitempty"` @@ -29,26 +32,27 @@ type Type struct { Labels TypeLabels `json:"labels,omitempty"` } +// Types represents the assigned types for each item type. type Types struct { Post Type `json:"post,omitempty"` Page Type `json:"page,omitempty"` Attachment Type `json:"attachment,omitempty"` } -type TypesCollection struct { - client *Client - url string -} +// TypesService provides access to the Type related functions in the WordPress REST API. +type TypesService service -func (col *TypesCollection) List(params interface{}) (*Types, *http.Response, []byte, error) { +// List returns a list of types. +func (c *TypesService) List(ctx context.Context, params interface{}) (*Types, *Response, error) { var types Types - resp, body, err := col.client.List(col.url, params, &types) - return &types, resp, body, err + resp, err := c.client.List(ctx, "types", params, &types) + return &types, resp, err } -func (col *TypesCollection) Get(slug string, params interface{}) (*Type, *http.Response, []byte, error) { +// Get returns a single type for the given id. +func (c *TypesService) Get(ctx context.Context, slug string, params interface{}) (*Type, *Response, error) { var entity Type - entityURL := fmt.Sprintf("%v/%v", col.url, slug) - resp, body, err := col.client.Get(entityURL, params, &entity) - return &entity, resp, body, err + entityURL := fmt.Sprintf("types/%v", slug) + resp, err := c.client.Get(ctx, entityURL, params, &entity) + return &entity, resp, err } diff --git a/types_test.go b/types_test.go index 28497ce..83dd002 100644 --- a/types_test.go +++ b/types_test.go @@ -6,36 +6,32 @@ import ( ) func TestTypesList(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - types, resp, body, err := wp.Types().List(nil) + types, resp, err := wp.Types.List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if types == nil { t.Errorf("Should not return nil types") } } func TestTypesGet(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - wpType, resp, body, err := wp.Types().Get("post", nil) + wpType, resp, err := wp.Types.Get(ctx, "post", nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } - if body == nil { - t.Errorf("Should not return nil body") - } + if wpType == nil { t.Errorf("Should not return nil type") } diff --git a/users.go b/users.go index dcf8186..baa26e3 100644 --- a/users.go +++ b/users.go @@ -1,15 +1,18 @@ package wordpress import ( + "context" "fmt" - "net/http" ) +// AvatarURLS returns different sizes of the users avatar. type AvatarURLS struct { Size24 string `json:"24,omitempty"` Size48 string `json:"48,omitempty"` Size96 string `json:"96,omitempty"` } + +// User represents a WordPress user. type User struct { ID int `json:"id,omitempty"` AvatarURL string `json:"avatar_url,omitempty"` @@ -23,50 +26,74 @@ type User struct { Link string `json:"link,omitempty"` Name string `json:"name,omitempty"` Nickname string `json:"nickname,omitempty"` - RegisteredDate string `json:"registered_date,omitempty"` + RegisteredDate Time `json:"registered_date,omitempty"` Roles []string `json:"roles,omitempty"` Slug string `json:"slug,omitempty"` URL string `json:"url,omitempty"` Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` + Locale string `json:"locale,omitempty"` } -type UsersCollection struct { - client *Client - url string -} +// UsersService provides access to the Users related functions in the WordPress REST API. +type UsersService service -func (col *UsersCollection) Me(params interface{}) (*User, *http.Response, []byte, error) { - url := fmt.Sprintf("%v/me", col.url) +// Me returns information about the currently authenticated user. +func (c *UsersService) Me(ctx context.Context, params interface{}) (*User, *Response, error) { + url := fmt.Sprintf("%v/me", "users") var user User - resp, body, err := col.client.Get(url, params, &user) - return &user, resp, body, err + resp, err := c.client.Get(ctx, url, params, &user) + return &user, resp, err } -func (col *UsersCollection) List(params interface{}) ([]User, *http.Response, []byte, error) { - var users []User - resp, body, err := col.client.List(col.url, params, &users) - return users, resp, body, err + +// List returns a list of users. +func (c *UsersService) List(ctx context.Context, opts *UserListOptions) ([]*User, *Response, error) { + u, err := addOptions("users", opts) + if err != nil { + return nil, nil, err + } + + req, err := c.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + users := []*User{} + resp, err := c.client.Do(ctx, req, &users) + if err != nil { + return nil, resp, err + } + + return users, resp, nil } -func (col *UsersCollection) Create(new *User) (*User, *http.Response, []byte, error) { + +// Create creates a new user. +func (c *UsersService) Create(ctx context.Context, newUser *User) (*User, *Response, error) { var created User - resp, body, err := col.client.Create(col.url, new, &created) - return &created, resp, body, err + resp, err := c.client.Create(ctx, "users", newUser, &created) + return &created, resp, err } -func (col *UsersCollection) Get(id int, params interface{}) (*User, *http.Response, []byte, error) { + +// Get returns a single user for the given id. +func (c *UsersService) Get(ctx context.Context, id int, params interface{}) (*User, *Response, error) { var entity User - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Get(entityURL, params, &entity) - return &entity, resp, body, err + entityURL := fmt.Sprintf("users/%v", id) + resp, err := c.client.Get(ctx, entityURL, params, &entity) + return &entity, resp, err } -func (col *UsersCollection) Update(id int, post *User) (*User, *http.Response, []byte, error) { + +// Update updates a single user with the given id. +func (c *UsersService) Update(ctx context.Context, id int, user *User) (*User, *Response, error) { var updated User - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Update(entityURL, post, &updated) - return &updated, resp, body, err + entityURL := fmt.Sprintf("users/%v", id) + resp, err := c.client.Update(ctx, entityURL, user, &updated) + return &updated, resp, err } -func (col *UsersCollection) Delete(id int, params interface{}) (*User, *http.Response, []byte, error) { + +// Delete removes the user with the given id. +func (c *UsersService) Delete(ctx context.Context, id int, params interface{}) (*User, *Response, error) { var deleted User - entityURL := fmt.Sprintf("%v/%v", col.url, id) - resp, body, err := col.client.Delete(entityURL, params, &deleted) - return &deleted, resp, body, err + entityURL := fmt.Sprintf("users/%v", id) + resp, err := c.client.Delete(ctx, entityURL, params, &deleted) + return &deleted, resp, err } diff --git a/users_test.go b/users_test.go index 48b37d6..eb2b311 100644 --- a/users_test.go +++ b/users_test.go @@ -1,9 +1,11 @@ package wordpress_test import ( - "github.com/sogko/go-wordpress" + "context" "net/http" "testing" + + "github.com/robbiet480/go-wordpress" ) func factoryUser() *wordpress.User { @@ -15,28 +17,27 @@ func factoryUser() *wordpress.User { Password: "password", } } + func cleanUpUser(t *testing.T, userID int) { - wp := initTestClient() + wp, ctx := initTestClient() // Note that deleting a user requires `force=true` since `users` resource does not support trashing - deletedUser, resp, body, err := wp.Users().Delete(userID, "force=true") + deletedUser, resp, err := wp.Users.Delete(ctx, userID, "force=true&reassign=1") if err != nil { t.Errorf("Failed to clean up new user: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if deletedUser.ID != userID { t.Errorf("Deleted user ID should be the same as newly created user: %v != %v", deletedUser.ID, userID) } } -func getAnyOneUser(t *testing.T, wp *wordpress.Client) *wordpress.User { - users, resp, _, _ := wp.Users().List(nil) - if resp.StatusCode != http.StatusOK { +func getAnyOneUser(t *testing.T, ctx context.Context, wp *wordpress.Client) *wordpress.User { + + users, resp, _ := wp.Users.List(ctx, nil) + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if len(users) < 1 { @@ -45,24 +46,21 @@ func getAnyOneUser(t *testing.T, wp *wordpress.Client) *wordpress.User { userID := users[0].ID - user, resp, _, _ := wp.Users().Get(userID, "context=edit") - if resp.StatusCode != http.StatusOK { + user, resp, _ := wp.Users.Get(ctx, userID, "context=edit") + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } return user } func TestUsersList(t *testing.T) { - client := initTestClient() + client, ctx := initTestClient() - users, resp, body, err := client.Users().List(nil) + users, resp, err := client.Users.List(ctx, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if users == nil { @@ -71,16 +69,13 @@ func TestUsersList(t *testing.T) { } func TestUsersMe(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - currentUser, resp, body, err := wp.Users().Me("context=edit") + currentUser, resp, err := wp.Users.Me(ctx, "context=edit") if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if currentUser == nil { @@ -92,18 +87,15 @@ func TestUsersMe(t *testing.T) { } func TestUsersGet(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() - u := getAnyOneUser(t, wp) + u := getAnyOneUser(t, ctx, wp) - user, resp, body, err := wp.Users().Get(u.ID, nil) + user, resp, err := wp.Users.Get(ctx, u.ID, nil) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 StatusOK, got %v", resp.Status) } if user == nil { @@ -112,7 +104,7 @@ func TestUsersGet(t *testing.T) { } func TestUsersCreate(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() u := &wordpress.User{ Username: "go-wordpress-test-user1", @@ -122,14 +114,11 @@ func TestUsersCreate(t *testing.T) { Password: "password", } - newUser, resp, body, err := wp.Users().Create(u) + newUser, resp, err := wp.Users.Create(ctx, u) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusCreated { + if resp != nil && resp.StatusCode != http.StatusCreated { t.Errorf("Expected 201 Created, got %v", resp.Status) } if newUser == nil { @@ -141,11 +130,11 @@ func TestUsersCreate(t *testing.T) { } func TestUsersDelete(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() u := factoryUser() - newUser, resp, _, _ := wp.Users().Create(u) - if resp.StatusCode != http.StatusCreated { + newUser, resp, _ := wp.Users.Create(ctx, u) + if resp != nil && resp.StatusCode != http.StatusCreated { t.Errorf("Expected 201 Created, got %v", resp.Status) } if newUser == nil { @@ -154,14 +143,11 @@ func TestUsersDelete(t *testing.T) { // Note that deleting a user requires `force=true` since `users` resource does not support trashing // If not specified, a 501 NotImplemented will be returned - deletedUser, resp, body, err := wp.Users().Delete(newUser.ID, "force=true") + deletedUser, resp, err := wp.Users.Delete(ctx, newUser.ID, "force=true&reassign=1") if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if deletedUser.ID != newUser.ID { @@ -170,12 +156,12 @@ func TestUsersDelete(t *testing.T) { } func TestUsersUpdate(t *testing.T) { - wp := initTestClient() + wp, ctx := initTestClient() u := factoryUser() // create user - newUser, resp, _, _ := wp.Users().Create(u) - if resp.StatusCode != http.StatusCreated { + newUser, resp, _ := wp.Users.Create(ctx, u) + if resp != nil && resp.StatusCode != http.StatusCreated { t.Errorf("Expected 201 Created, got %v", resp.Status) } if newUser == nil { @@ -183,8 +169,8 @@ func TestUsersUpdate(t *testing.T) { } // get user in `edit` context - user, resp, _, _ := wp.Users().Get(newUser.ID, "context=edit") - if resp.StatusCode != http.StatusOK { + user, resp, _ := wp.Users.Get(ctx, newUser.ID, "context=edit") + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if user == nil { @@ -199,14 +185,11 @@ func TestUsersUpdate(t *testing.T) { user.Email = newUserEmail // update - updatedUser, resp, body, err := wp.Users().Update(user.ID, user) + updatedUser, resp, err := wp.Users.Update(ctx, user.ID, user) if err != nil { t.Errorf("Should not return error: %v", err.Error()) } - if body == nil { - t.Errorf("body should not be nil") - } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { t.Errorf("Expected 200 OK, got %v", resp.Status) } if updatedUser.ID != user.ID { diff --git a/utils.go b/utils.go deleted file mode 100644 index f2a1654..0000000 --- a/utils.go +++ /dev/null @@ -1,62 +0,0 @@ -package wordpress - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "github.com/parnurzeal/gorequest" - "log" - "net/http" - "os" -) - -var DEBUG bool = (os.Getenv("DEBUG") == "1") - -func unmarshallResponse(resp gorequest.Response, body []byte, result interface{}) error { - - var prettyJSON bytes.Buffer - err2 := json.Indent(&prettyJSON, body, "", " ") - if err2 != nil { - log.Println("JSON parse error: ", err2) - - if DEBUG { - log.Println("body: ", string(body)) - } - } - if DEBUG { - log.Println("body: ", string(prettyJSON.Bytes())) - } - - if resp.StatusCode != http.StatusOK && - resp.StatusCode != http.StatusCreated && - resp.StatusCode != http.StatusAccepted { - return errors.New(resp.Status) - } - - err := json.Unmarshal(body, result) - if err != nil { - log.Println("JSON parse error: ", err) - return err - } - return nil -} - -func _warning(v ...interface{}) { - log.Println(fmt.Sprintln("[go-wordpress]", v)) -} - -func _log(v ...interface{}) { - log.Println(fmt.Sprintln("[go-wordpress]", v)) -} - -// UnmarshallServerError A helper function to unmarshall error response from server -func UnmarshallServerError(body []byte) ([]GeneralError, error) { - var resp []GeneralError - err := json.Unmarshal(body, &resp) - if err != nil { - log.Println("JSON parse error: ", err) - return nil, err - } - return resp, nil -} diff --git a/wp_api_json_schema.json b/wp_api_json_schema.json new file mode 100644 index 0000000..048ab05 --- /dev/null +++ b/wp_api_json_schema.json @@ -0,0 +1,858 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "title": "WordPress REST API", + "definitions": { + "UsersListOptions": { + "properties": { + "context": { + "default": "view", + "enum": [ + "view", + "embed", + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "page": { + "default": 1, + "description": "Current page of the collection.", + "type": "integer" + }, + "per_page": { + "default": 10, + "description": "Maximum number of items to be returned in result set.", + "type": "integer" + }, + "search": { + "description": "Limit results to those matching a string.", + "type": "string" + }, + "exclude": { + "default": [], + "description": "Ensure result set excludes specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "include": { + "default": [], + "description": "Limit result set to specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "offset": { + "description": "Offset the result set by a specific number of items.", + "type": "integer" + }, + "order": { + "default": "asc", + "enum": [ + "asc", + "desc" + ], + "description": "Order sort attribute ascending or descending.", + "type": "string" + }, + "orderby": { + "default": "name", + "enum": [ + "id", + "include", + "name", + "registered_date", + "slug", + "include_slugs", + "email", + "url" + ], + "description": "Sort collection by object attribute.", + "type": "string" + }, + "slug": { + "description": "Limit result set to users with one or more specific slugs.", + "type": "array", + "items": { + "type": "string" + } + }, + "roles": { + "description": "Limit result set to users matching at least one specific role provided. Accepts csv list or single role.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PagesListOptions": { + "properties": { + "context": { + "default": "view", + "enum": [ + "view", + "embed", + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "page": { + "default": 1, + "description": "Current page of the collection.", + "type": "integer" + }, + "per_page": { + "default": 10, + "description": "Maximum number of items to be returned in result set.", + "type": "integer" + }, + "search": { + "description": "Limit results to those matching a string.", + "type": "string" + }, + "after": { + "description": "Limit response to posts published after a given ISO8601 compliant date.", + "type": "string" + }, + "author": { + "default": [], + "description": "Limit result set to posts assigned to specific authors.", + "type": "array", + "items": { + "type": "integer" + } + }, + "author_exclude": { + "default": [], + "description": "Ensure result set excludes posts assigned to specific authors.", + "type": "array", + "items": { + "type": "integer" + } + }, + "before": { + "description": "Limit response to posts published before a given ISO8601 compliant date.", + "type": "string" + }, + "exclude": { + "default": [], + "description": "Ensure result set excludes specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "include": { + "default": [], + "description": "Limit result set to specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "menu_order": { + "description": "Limit result set to posts with a specific menu_order value.", + "type": "integer" + }, + "offset": { + "description": "Offset the result set by a specific number of items.", + "type": "integer" + }, + "order": { + "default": "desc", + "enum": [ + "asc", + "desc" + ], + "description": "Order sort attribute ascending or descending.", + "type": "string" + }, + "orderby": { + "default": "date", + "enum": [ + "author", + "date", + "id", + "include", + "modified", + "parent", + "relevance", + "slug", + "include_slugs", + "title", + "menu_order" + ], + "description": "Sort collection by object attribute.", + "type": "string" + }, + "parent": { + "default": [], + "description": "Limit result set to items with particular parent IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "parent_exclude": { + "default": [], + "description": "Limit result set to all items except those of a particular parent ID.", + "type": "array", + "items": { + "type": "integer" + } + }, + "slug": { + "description": "Limit result set to posts with one or more specific slugs.", + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "default": "publish", + "description": "Limit result set to posts assigned one or more statuses.", + "type": "array", + "items": { + "enum": [ + "publish", + "future", + "draft", + "pending", + "private", + "trash", + "auto-draft", + "inherit", + "any" + ], + "type": "string" + } + } + } + }, + "TagsListOptions": { + "properties": { + "context": { + "default": "view", + "enum": [ + "view", + "embed", + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "page": { + "default": 1, + "description": "Current page of the collection.", + "type": "integer" + }, + "per_page": { + "default": 10, + "description": "Maximum number of items to be returned in result set.", + "type": "integer" + }, + "search": { + "description": "Limit results to those matching a string.", + "type": "string" + }, + "exclude": { + "default": [], + "description": "Ensure result set excludes specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "include": { + "default": [], + "description": "Limit result set to specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "offset": { + "description": "Offset the result set by a specific number of items.", + "type": "integer" + }, + "order": { + "default": "asc", + "enum": [ + "asc", + "desc" + ], + "description": "Order sort attribute ascending or descending.", + "type": "string" + }, + "orderby": { + "default": "name", + "enum": [ + "id", + "include", + "name", + "slug", + "include_slugs", + "term_group", + "description", + "count" + ], + "description": "Sort collection by term attribute.", + "type": "string" + }, + "hide_empty": { + "default": false, + "description": "Whether to hide terms not assigned to any posts.", + "type": "boolean" + }, + "post": { + "description": "Limit result set to terms assigned to a specific post.", + "type": "integer" + }, + "slug": { + "description": "Limit result set to terms with one or more specific slugs.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CategoriesListOptions": { + "properties": { + "context": { + "default": "view", + "enum": [ + "view", + "embed", + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "page": { + "default": 1, + "description": "Current page of the collection.", + "type": "integer" + }, + "per_page": { + "default": 10, + "description": "Maximum number of items to be returned in result set.", + "type": "integer" + }, + "search": { + "description": "Limit results to those matching a string.", + "type": "string" + }, + "exclude": { + "default": [], + "description": "Ensure result set excludes specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "include": { + "default": [], + "description": "Limit result set to specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "order": { + "default": "asc", + "enum": [ + "asc", + "desc" + ], + "description": "Order sort attribute ascending or descending.", + "type": "string" + }, + "orderby": { + "default": "name", + "enum": [ + "id", + "include", + "name", + "slug", + "include_slugs", + "term_group", + "description", + "count" + ], + "description": "Sort collection by term attribute.", + "type": "string" + }, + "hide_empty": { + "default": false, + "description": "Whether to hide terms not assigned to any posts.", + "type": "boolean" + }, + "parent": { + "description": "Limit result set to terms assigned to a specific parent.", + "type": "integer" + }, + "post": { + "description": "Limit result set to terms assigned to a specific post.", + "type": "integer" + }, + "slug": { + "description": "Limit result set to terms with one or more specific slugs.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CommentsListOptions": { + "properties": { + "context": { + "default": "view", + "enum": [ + "view", + "embed", + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "page": { + "default": 1, + "description": "Current page of the collection.", + "type": "integer" + }, + "per_page": { + "default": 10, + "description": "Maximum number of items to be returned in result set.", + "type": "integer" + }, + "search": { + "description": "Limit results to those matching a string.", + "type": "string" + }, + "after": { + "description": "Limit response to comments published after a given ISO8601 compliant date.", + "type": "string" + }, + "author": { + "description": "Limit result set to comments assigned to specific user IDs. Requires authorization.", + "type": "array", + "items": { + "type": "integer" + } + }, + "author_exclude": { + "description": "Ensure result set excludes comments assigned to specific user IDs. Requires authorization.", + "type": "array", + "items": { + "type": "integer" + } + }, + "author_email": { + "description": "Limit result set to that from a specific author email. Requires authorization.", + "type": "string" + }, + "before": { + "description": "Limit response to comments published before a given ISO8601 compliant date.", + "type": "string" + }, + "exclude": { + "default": [], + "description": "Ensure result set excludes specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "include": { + "default": [], + "description": "Limit result set to specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "offset": { + "description": "Offset the result set by a specific number of items.", + "type": "integer" + }, + "order": { + "default": "desc", + "enum": [ + "asc", + "desc" + ], + "description": "Order sort attribute ascending or descending.", + "type": "string" + }, + "orderby": { + "default": "date_gmt", + "enum": [ + "date", + "date_gmt", + "id", + "include", + "post", + "parent", + "type" + ], + "description": "Sort collection by object attribute.", + "type": "string" + }, + "parent": { + "default": [], + "description": "Limit result set to comments of specific parent IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "parent_exclude": { + "default": [], + "description": "Ensure result set excludes specific parent IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "post": { + "default": [], + "description": "Limit result set to comments assigned to specific post IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "status": { + "default": "approve", + "description": "Limit result set to comments assigned a specific status. Requires authorization.", + "type": "string" + }, + "type": { + "default": "comment", + "description": "Limit result set to comments assigned a specific type. Requires authorization.", + "type": "string" + }, + "password": { + "description": "The password for the post if it is password protected.", + "type": "string" + } + } + }, + "MediaListOptions": { + "properties": { + "context": { + "default": "view", + "enum": [ + "view", + "embed", + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "page": { + "default": 1, + "description": "Current page of the collection.", + "type": "integer" + }, + "per_page": { + "default": 10, + "description": "Maximum number of items to be returned in result set.", + "type": "integer" + }, + "search": { + "description": "Limit results to those matching a string.", + "type": "string" + }, + "after": { + "description": "Limit response to posts published after a given ISO8601 compliant date.", + "type": "string" + }, + "author": { + "default": [], + "description": "Limit result set to posts assigned to specific authors.", + "type": "array", + "items": { + "type": "integer" + } + }, + "author_exclude": { + "default": [], + "description": "Ensure result set excludes posts assigned to specific authors.", + "type": "array", + "items": { + "type": "integer" + } + }, + "before": { + "description": "Limit response to posts published before a given ISO8601 compliant date.", + "type": "string" + }, + "exclude": { + "default": [], + "description": "Ensure result set excludes specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "include": { + "default": [], + "description": "Limit result set to specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "offset": { + "description": "Offset the result set by a specific number of items.", + "type": "integer" + }, + "order": { + "default": "desc", + "enum": [ + "asc", + "desc" + ], + "description": "Order sort attribute ascending or descending.", + "type": "string" + }, + "orderby": { + "default": "date", + "enum": [ + "author", + "date", + "id", + "include", + "modified", + "parent", + "relevance", + "slug", + "include_slugs", + "title" + ], + "description": "Sort collection by object attribute.", + "type": "string" + }, + "parent": { + "default": [], + "description": "Limit result set to items with particular parent IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "parent_exclude": { + "default": [], + "description": "Limit result set to all items except those of a particular parent ID.", + "type": "array", + "items": { + "type": "integer" + } + }, + "slug": { + "description": "Limit result set to posts with one or more specific slugs.", + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "default": "inherit", + "description": "Limit result set to posts assigned one or more statuses.", + "type": "array", + "items": { + "enum": [ + "inherit", + "private", + "trash" + ], + "type": "string" + } + }, + "media_type": { + "enum": [ + "image", + "video", + "text", + "application", + "audio" + ], + "description": "Limit result set to attachments of a particular media type.", + "type": "string" + }, + "mime_type": { + "description": "Limit result set to attachments of a particular MIME type.", + "type": "string" + } + } + }, + "PostsListOptions": { + "properties": { + "context": { + "default": "view", + "enum": [ + "view", + "embed", + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "page": { + "default": 1, + "description": "Current page of the collection.", + "type": "integer" + }, + "per_page": { + "default": 10, + "description": "Maximum number of items to be returned in result set.", + "type": "integer" + }, + "search": { + "description": "Limit results to those matching a string.", + "type": "string" + }, + "after": { + "description": "Limit response to posts published after a given ISO8601 compliant date.", + "type": "string" + }, + "author": { + "default": [], + "description": "Limit result set to posts assigned to specific authors.", + "type": "array", + "items": { + "type": "integer" + } + }, + "author_exclude": { + "default": [], + "description": "Ensure result set excludes posts assigned to specific authors.", + "type": "array", + "items": { + "type": "integer" + } + }, + "before": { + "description": "Limit response to posts published before a given ISO8601 compliant date.", + "type": "string" + }, + "exclude": { + "default": [], + "description": "Ensure result set excludes specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "include": { + "default": [], + "description": "Limit result set to specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "offset": { + "description": "Offset the result set by a specific number of items.", + "type": "integer" + }, + "order": { + "default": "desc", + "enum": [ + "asc", + "desc" + ], + "description": "Order sort attribute ascending or descending.", + "type": "string" + }, + "orderby": { + "default": "date", + "enum": [ + "author", + "date", + "id", + "include", + "modified", + "parent", + "relevance", + "slug", + "include_slugs", + "title" + ], + "description": "Sort collection by object attribute.", + "type": "string" + }, + "slug": { + "description": "Limit result set to posts with one or more specific slugs.", + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "default": "publish", + "description": "Limit result set to posts assigned one or more statuses.", + "type": "array", + "items": { + "enum": [ + "publish", + "future", + "draft", + "pending", + "private", + "trash", + "auto-draft", + "inherit", + "any" + ], + "type": "string" + } + }, + "categories": { + "default": [], + "description": "Limit result set to all items that have the specified term assigned in the categories taxonomy.", + "type": "array", + "items": { + "type": "integer" + } + }, + "categories_exclude": { + "default": [], + "description": "Limit result set to all items except those that have the specified term assigned in the categories taxonomy.", + "type": "array", + "items": { + "type": "integer" + } + }, + "tags": { + "default": [], + "description": "Limit result set to all items that have the specified term assigned in the tags taxonomy.", + "type": "array", + "items": { + "type": "integer" + } + }, + "tags_exclude": { + "default": [], + "description": "Limit result set to all items except those that have the specified term assigned in the tags taxonomy.", + "type": "array", + "items": { + "type": "integer" + } + }, + "sticky": { + "description": "Limit result set to items that are sticky.", + "type": "boolean" + } + } + } + } +}