wordpress_client 0.0.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +30 -0
  3. data/.codeclimate.yml +23 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +147 -90
  6. data/.yardopts +1 -0
  7. data/Changelog.md +16 -0
  8. data/Gemfile +6 -1
  9. data/README.md +48 -14
  10. data/Rakefile +37 -0
  11. data/lib/wordpress_client/category.rb +2 -0
  12. data/lib/wordpress_client/client.rb +235 -79
  13. data/lib/wordpress_client/connection.rb +10 -10
  14. data/lib/wordpress_client/errors.rb +41 -6
  15. data/lib/wordpress_client/media.rb +49 -1
  16. data/lib/wordpress_client/media_parser.rb +3 -0
  17. data/lib/wordpress_client/paginated_collection.rb +61 -4
  18. data/lib/wordpress_client/post.rb +63 -16
  19. data/lib/wordpress_client/post_parser.rb +12 -39
  20. data/lib/wordpress_client/rest_parser.rb +4 -0
  21. data/lib/wordpress_client/tag.rb +2 -0
  22. data/lib/wordpress_client/term.rb +30 -0
  23. data/lib/wordpress_client/version.rb +5 -1
  24. data/lib/wordpress_client.rb +21 -5
  25. data/spec/client_spec.rb +17 -181
  26. data/spec/connection_spec.rb +15 -14
  27. data/spec/docker/Dockerfile +35 -10
  28. data/spec/docker/README.md +53 -16
  29. data/spec/docker/dbdump.sql.gz +0 -0
  30. data/spec/docker/restore-dbdump.sh +2 -2
  31. data/spec/docker/yum.repos.d/CentOS-Base.repo +25 -0
  32. data/spec/fixtures/image-media.json +1 -1
  33. data/spec/fixtures/post-with-metadata.json +99 -1
  34. data/spec/fixtures/simple-post.json +324 -1
  35. data/spec/integration/attachments_crud_spec.rb +1 -1
  36. data/spec/integration/posts_crud_spec.rb +1 -1
  37. data/spec/integration/posts_finding_spec.rb +0 -69
  38. data/spec/integration/posts_metadata_spec.rb +11 -11
  39. data/spec/integration/posts_with_attachments_spec.rb +20 -6
  40. data/spec/media_spec.rb +14 -0
  41. data/spec/post_spec.rb +5 -31
  42. data/spec/spec_helper.rb +1 -0
  43. data/spec/support/docker_runner.rb +33 -13
  44. data/spec/support/wordpress_server.rb +112 -74
  45. data/wordpress_client.gemspec +17 -17
  46. metadata +43 -31
  47. data/.hound.yml +0 -2
  48. data/.ruby-version +0 -1
  49. data/circle.yml +0 -3
  50. data/lib/wordpress_client/replace_metadata.rb +0 -81
  51. data/lib/wordpress_client/replace_terms.rb +0 -62
  52. data/spec/fixtures/post-with-forbidden-metadata.json +0 -1
  53. data/spec/integration/category_assignment_spec.rb +0 -29
  54. data/spec/integration/tag_assignment_spec.rb +0 -29
  55. data/spec/replace_metadata_spec.rb +0 -56
  56. data/spec/replace_terms_spec.rb +0 -51
@@ -1,19 +1,58 @@
1
1
  module WordpressClient
2
+ # Represents a media record in Wordpress.
2
3
  class Media
3
4
  attr_accessor(
4
- :id, :slug, :title_html, :description,
5
+ :id, :slug, :media_type, :title_html, :description, :alt_text,
5
6
  :date, :updated_at,
6
7
  :guid, :link, :media_details
7
8
  )
8
9
 
10
+ # @!attribute [r] media_type
11
+ # @return [String] the type of the media
12
+ # @example
13
+ # media.media_type #=> "image"
14
+
15
+ # @!attribute [rw] title_html
16
+ # @return [String] the title of the media, HTML escaped
17
+ # @example
18
+ # media.title_html #=> "Sunset — Painting by some person"
19
+
20
+ # @!attribute [rw] date
21
+ # @return [Time, nil] the date of the media, in UTC if available
22
+
23
+ # @!attribute [rw] updated_at
24
+ # @return [Time, nil] the modification date of the media, in UTC if available
25
+
26
+ # @!attribute [rw] guid
27
+ # Returns the permalink/GUID – or +source_url+ – of the media.
28
+ #
29
+ # Media that are embedded in posts have a +source_url+ attribute and no
30
+ # +guid+, and stand-alone media has a +guid+ but no +source_url+. They
31
+ # are both backed by the same data, so this method handles both cases,
32
+ # and is aliased to both names.
33
+ #
34
+ # @return [String] the permalink/GUID – or +source_url+ – of the media
35
+
36
+ # @!attribute [rw] media_details
37
+ # Returns the media details if available.
38
+ #
39
+ # Media details cannot be documented here. It's up to you to handle this
40
+ # generic "payload" attribute the best way you can.
41
+ #
42
+ # @return [Hash<String,Object>] the media details returned from the server
43
+
44
+ # @api private
9
45
  def self.parse(data)
10
46
  MediaParser.parse(data)
11
47
  end
12
48
 
49
+ # Creates a new instance, populating the fields with the passed values.
13
50
  def initialize(
14
51
  id: nil,
15
52
  slug: nil,
53
+ media_type: nil,
16
54
  title_html: nil,
55
+ alt_text: nil,
17
56
  description: nil,
18
57
  date: nil,
19
58
  updated_at: nil,
@@ -23,9 +62,11 @@ module WordpressClient
23
62
  )
24
63
  @id = id
25
64
  @slug = slug
65
+ @media_type = media_type
26
66
  @title_html = title_html
27
67
  @date = date
28
68
  @updated_at = updated_at
69
+ @alt_text = alt_text
29
70
  @description = description
30
71
  @guid = guid
31
72
  @link = link
@@ -33,5 +74,12 @@ module WordpressClient
33
74
  end
34
75
 
35
76
  alias source_url guid
77
+
78
+ # Returns the same +Media+ instance if it is an image, else +nil+.
79
+ def as_image
80
+ if media_type == "image"
81
+ self
82
+ end
83
+ end
36
84
  end
37
85
  end
@@ -1,4 +1,5 @@
1
1
  module WordpressClient
2
+ # @private
2
3
  class MediaParser
3
4
  include RestParser
4
5
 
@@ -26,8 +27,10 @@ module WordpressClient
26
27
 
27
28
  def assign_basic(media)
28
29
  media.id = data.fetch("id")
30
+ media.media_type = data.fetch("media_type")
29
31
  media.slug = data.fetch("slug")
30
32
  media.link = data.fetch("link")
33
+ media.alt_text = data.fetch("alt_text")
31
34
  media.description = data["description"]
32
35
  media.media_details = data["media_details"]
33
36
  end
@@ -1,9 +1,31 @@
1
1
  require "delegate"
2
2
 
3
3
  module WordpressClient
4
+ # Represents a paginated list of resources.
5
+ #
6
+ # @note This class has the full +Array+ interface by using
7
+ # +DelegateClass(Array)+. Methods do not show up in the documentation
8
+ # unless when manually documented.
4
9
  class PaginatedCollection < DelegateClass(Array)
10
+ # @!method size
11
+ # @return [Fixnum] the number of records actually in this "page".
12
+ # @see Array#size
13
+
5
14
  attr_reader :total, :current_page, :per_page
6
15
 
16
+ # @!attribute [r] total
17
+ # @return [Fixnum] the total hits in the full collection.
18
+ # @see #size #size is the size of the current "page".
19
+
20
+ # @!attribute [r] current_page
21
+ # @return [Fixnum] the current page number, where +1+ is the first page.
22
+
23
+ # @!attribute [r] per_page
24
+ # @return [Fixnum] the current page size setting, for example +30+.
25
+
26
+ # Create a new collection using the passed array +entries+.
27
+ #
28
+ # @param entries [Array] the original "page" array
7
29
  def initialize(entries, total:, current_page:, per_page:)
8
30
  super(entries)
9
31
  @total = total
@@ -11,12 +33,15 @@ module WordpressClient
11
33
  @per_page = per_page
12
34
  end
13
35
 
14
- #
15
- # Pagination methods. Fulfilling will_paginate protocol
16
- #
36
+ # @!group will_paginate protocol
17
37
 
18
38
  alias total_entries total
19
39
 
40
+ # @note This method is used by +will_paginate+. By implementing this
41
+ # interface, you can use a {PaginatedCollection} in place of a
42
+ # +WillPaginate::Collection+ to render pagination details.
43
+ # @return [Fixnum] the total number of pages that can show the {#total}
44
+ # entries with {#per_page} records per page. +0+ if no entries.
20
45
  def total_pages
21
46
  if total.zero? || per_page.zero?
22
47
  0
@@ -25,23 +50,53 @@ module WordpressClient
25
50
  end
26
51
  end
27
52
 
53
+ # @note This method is used by +will_paginate+. By implementing this
54
+ # interface, you can use a {PaginatedCollection} in place of a
55
+ # +WillPaginate::Collection+ to render pagination details.
56
+ # @return [Fixnum, nil] the next page number or +nil+ if on last page.
28
57
  def next_page
29
58
  if current_page < total_pages
30
59
  current_page + 1
31
60
  end
32
61
  end
33
62
 
63
+ # @note This method is used by +will_paginate+. By implementing this
64
+ # interface, you can use a {PaginatedCollection} in place of a
65
+ # +WillPaginate::Collection+ to render pagination details.
66
+ # @return [Fixnum, nil] the previous page number or +nil+ if on first page.
34
67
  def previous_page
35
68
  if current_page > 1
36
69
  current_page - 1
37
70
  end
38
71
  end
39
72
 
73
+ # @note This method is used by +will_paginate+. By implementing this
74
+ # interface, you can use a {PaginatedCollection} in place of a
75
+ # +WillPaginate::Collection+ to render pagination details.
76
+ # @return [Boolean] if the current page is out of bounds, e.g. less than 1
77
+ # or higher than {#total_pages}.
40
78
  def out_of_bounds?
41
79
  current_page < 1 || current_page > total_pages
42
80
  end
43
81
 
44
- # will_paginate < 3.0 has this method
82
+ # @note This method is used by +will_paginate+. By implementing this
83
+ # interface, you can use a {PaginatedCollection} in place of a
84
+ # +WillPaginate::Collection+ to render pagination details.
85
+ #
86
+ # @note will_paginate < 3.0 has this method, but it's no longer present in
87
+ # newer will_paginate.
88
+ #
89
+ # Returns the offset of the current page.
90
+ #
91
+ # @example First page offset
92
+ # collection.per_page # => 20
93
+ # collection.current_page # => 1
94
+ # collection.offset #=> 0
95
+ # @example Later offset
96
+ # collection.per_page # => 20
97
+ # collection.current_page # => 3
98
+ # collection.offset #=> 40
99
+ #
45
100
  def offset
46
101
  if current_page > 0
47
102
  (current_page - 1) * per_page
@@ -49,5 +104,7 @@ module WordpressClient
49
104
  0
50
105
  end
51
106
  end
107
+
108
+ # @!endgroup
52
109
  end
53
110
  end
@@ -1,18 +1,68 @@
1
1
  require "time"
2
2
 
3
3
  module WordpressClient
4
+ # Represents a post in Wordpress.
5
+ #
6
+ # @see http://v2.wp-api.org/reference/posts/ API documentation for Post
4
7
  class Post
5
8
  attr_accessor(
6
9
  :id, :slug, :url, :guid, :status,
7
10
  :title_html, :excerpt_html, :content_html,
8
11
  :updated_at, :date,
9
- :categories, :tags, :meta, :featured_image
12
+ :categories, :tags, :meta, :featured_media,
13
+ :tag_ids, :category_ids, :featured_media_id
10
14
  )
11
15
 
16
+ # @!attribute [rw] title_html
17
+ # @return [String] the title of the media, HTML escaped
18
+ # @example
19
+ # post.title_html #=> "Fire &#038; diamonds!"
20
+
21
+ # @!attribute [rw] date
22
+ # @return [Time, nil] the date of the post, in UTC if available
23
+
24
+ # @!attribute [rw] updated_at
25
+ # @return [Time, nil] the modification date of the post, in UTC if available
26
+
27
+ # @!attribute [rw] guid
28
+ # @return [String] the permalink/GUID of the post for internal addressing
29
+ # @see #url
30
+
31
+ # @!attribute [rw] url
32
+ # @return [String] the URL (link) to the post
33
+
34
+ # @!attribute [rw] status
35
+ # @return ["publish", "future", "draft", "pending", "private", nil] the
36
+ # current status of the post, or +nil+ if undetermined
37
+
38
+ # @!attribute [rw] categories
39
+ # @return [Array[Category]] the {Category Categories} the post belongs to.
40
+ # @see Category
41
+
42
+ # @!attribute [rw] tags
43
+ # @return [Array[Tag]] the {Tag Tags} the post belongs to.
44
+ # @see Tag
45
+
46
+ # @!attribute [rw] featured_media
47
+ # @return [Media, nil] the featured image, as an instance of {Media}
48
+ # @see Media
49
+
50
+ # @!attribute [rw] meta
51
+ # Returns the Post meta, as a +Hash+ of +String => String+.
52
+ #
53
+ # @example
54
+ # post.meta # => {"Mood" => "Happy", "reviewed_by" => "user:45"}
55
+ #
56
+ # @return [Hash<String,String>] the post meta, as a Hash.
57
+ # @see Category
58
+ # @see Client#update_post
59
+
60
+ # @api private
12
61
  def self.parse(data)
13
62
  PostParser.parse(data)
14
63
  end
15
64
 
65
+ # Construct a new instance with the given attributes.
16
66
  def initialize(
17
67
  id: nil,
18
68
  slug: nil,
@@ -26,9 +76,10 @@ module WordpressClient
26
76
  date: nil,
27
77
  categories: [],
28
78
  tags: [],
29
- featured_image: nil,
30
- meta: {},
31
- meta_ids: {}
79
+ category_ids: [],
80
+ tag_ids: [],
81
+ featured_media: nil,
82
+ meta: {}
32
83
  )
33
84
  @id = id
34
85
  @slug = slug
@@ -42,21 +93,17 @@ module WordpressClient
42
93
  @date = date
43
94
  @categories = categories
44
95
  @tags = tags
45
- @featured_image = featured_image
96
+ @category_ids = category_ids
97
+ @tag_ids = tag_ids
98
+ @featured_media = featured_media
46
99
  @meta = meta
47
- @meta_ids = meta_ids
48
- end
49
-
50
- def category_ids() categories.map(&:id) end
51
-
52
- def tag_ids() tags.map(&:id) end
53
-
54
- def featured_image_id
55
- featured_image && featured_image.id
56
100
  end
57
101
 
58
- def meta_id_for(key)
59
- @meta_ids[key] || raise(ArgumentError, "Post does not have meta #{key.inspect}")
102
+ # Returns the featured media, if the featured media is an image.
103
+ def featured_image
104
+ if featured_media
105
+ featured_media.as_image
106
+ end
60
107
  end
61
108
  end
62
109
  end
@@ -1,4 +1,5 @@
1
1
  module WordpressClient
2
+ # @private
2
3
  class PostParser
3
4
  include RestParser
4
5
 
@@ -12,16 +13,13 @@ module WordpressClient
12
13
  end
13
14
 
14
15
  def to_post
15
- meta, meta_ids = parse_metadata
16
- post = Post.new(meta: meta, meta_ids: meta_ids)
17
-
16
+ post = Post.new
18
17
  assign_basic(post)
19
18
  assign_dates(post)
20
19
  assign_rendered(post)
21
20
  assign_categories(post)
22
21
  assign_tags(post)
23
- assign_featured_image(post)
24
-
22
+ assign_featured_media(post)
25
23
  post
26
24
  end
27
25
 
@@ -33,6 +31,10 @@ module WordpressClient
33
31
  post.slug = data["slug"]
34
32
  post.url = data["link"]
35
33
  post.status = data["status"]
34
+ post.meta = data["meta"]
35
+ post.category_ids = data["categories"]
36
+ post.tag_ids = data["tags"]
37
+ post.featured_media_id = data["featured_media"]
36
38
  end
37
39
 
38
40
  def assign_dates(post)
@@ -59,34 +61,19 @@ module WordpressClient
59
61
  end
60
62
  end
61
63
 
62
- def assign_featured_image(post)
63
- featured_id = data["featured_image"]
64
+ def assign_featured_media(post)
65
+ featured_id = data["featured_media"]
64
66
  if featured_id
65
- features = (embedded["https://api.w.org/featuredmedia"] || []).flatten
67
+ features = (embedded["wp:featuredmedia"] || []).flatten
66
68
  media = features.detect { |feature| feature["id"] == featured_id }
67
69
  if media
68
- post.featured_image = Media.parse(media)
70
+ post.featured_media = Media.parse(media)
69
71
  end
70
72
  end
71
73
  end
72
74
 
73
- def parse_metadata
74
- embedded_metadata = (embedded["https://api.w.org/meta"] || []).flatten
75
- validate_embedded_metadata(embedded_metadata)
76
-
77
- meta = {}
78
- meta_ids = {}
79
-
80
- embedded_metadata.each do |entry|
81
- meta[entry.fetch("key")] = entry.fetch("value")
82
- meta_ids[entry.fetch("key")] = entry.fetch("id")
83
- end
84
-
85
- [meta, meta_ids]
86
- end
87
-
88
75
  def embedded_terms(type)
89
- term_collections = embedded["https://api.w.org/term"] || []
76
+ term_collections = embedded["wp:term"] || embedded["https://api.w.org/term"] || []
90
77
 
91
78
  # term_collections is an array of arrays with terms in them. We can see
92
79
  # the type of the "collection" by inspecting the first child's taxonomy.
@@ -95,19 +82,5 @@ module WordpressClient
95
82
  } || []
96
83
  end
97
84
 
98
- def validate_embedded_metadata(embedded_metadata)
99
- if embedded_metadata.size == 1 && embedded_metadata.first["code"]
100
- error = embedded_metadata.first
101
- case error["code"]
102
- when "rest_forbidden"
103
- raise UnauthorizedError, error.fetch(
104
- "message", "You are not authorized to see meta for this post."
105
- )
106
- else
107
- raise Error, "Could not retreive meta for this post: " \
108
- "#{error["code"]} – #{error["message"]}"
109
- end
110
- end
111
- end
112
85
  end
113
86
  end
@@ -1,4 +1,8 @@
1
1
  module WordpressClient
2
+ # @private
3
+ #
4
+ # Module included in other classes that need to parse result bodies from the
5
+ # WP API.
2
6
  module RestParser
3
7
  private
4
8
  def rendered(name)
@@ -1,4 +1,6 @@
1
1
  module WordpressClient
2
+ # Represents a tag from Wordpress.
3
+ # @see Term
2
4
  class Tag < Term
3
5
  end
4
6
  end
@@ -1,7 +1,27 @@
1
1
  module WordpressClient
2
+ # @abstract Implement a subclass for the resource type.
3
+ #
4
+ # Implementation for the abstract "term" in Wordpress.
5
+ #
6
+ # @see Category
7
+ # @see Tag
2
8
  class Term
3
9
  attr_reader :id, :name_html, :slug
4
10
 
11
+ # @!attribute [r] id
12
+ # @return [Fixnum] The ID of the resource in Wordpress.
13
+
14
+ # @!attribute [r] name_html
15
+ # @return [String] The name of the resource, HTML encoded.
16
+ # @example
17
+ # term.name_html #=> "Father &#038; Daughter stuff"
18
+
19
+ # @!attribute [r] slug
20
+ # @return [String] The slug of the resource in Wordpress.
21
+
22
+ # @api private
23
+ #
24
+ # Parses a data structure from a WP API response body into this term type.
5
25
  def self.parse(data)
6
26
  new(
7
27
  id: data.fetch("id"),
@@ -16,6 +36,15 @@ module WordpressClient
16
36
  @slug = slug
17
37
  end
18
38
 
39
+ # @api private
40
+ # Compares another instance. All attributes in this list must be equal for
41
+ # the instances to be equal:
42
+ #
43
+ # * +id+
44
+ # * +name_html+
45
+ # * +slug+
46
+ #
47
+ # One must also not be a subclass of the other; they must be the exact same class.
19
48
  def ==(other)
20
49
  if other.is_a? Term
21
50
  other.class == self.class &&
@@ -27,6 +56,7 @@ module WordpressClient
27
56
  end
28
57
  end
29
58
 
59
+ # Shows a nice representation of the term type.
30
60
  def inspect
31
61
  "#<#{self.class} ##{id} #{name_html.inspect} (#{slug})>"
32
62
  end
@@ -1,3 +1,7 @@
1
1
  module WordpressClient
2
- VERSION = "0.0.1"
2
+ # Current version of the gem.
3
+ #
4
+ # @note This only applies if using a released version. A development build
5
+ # would not correspond to this constant.
6
+ VERSION = "2.0.1".freeze
3
7
  end
@@ -15,11 +15,27 @@ require "wordpress_client/post_parser"
15
15
  require "wordpress_client/media"
16
16
  require "wordpress_client/media_parser"
17
17
 
18
- require "wordpress_client/replace_terms"
19
- require "wordpress_client/replace_metadata"
20
-
21
18
  module WordpressClient
22
- def self.new(*args)
23
- Client.new(Connection.new(*args))
19
+ # Initialize a new client using the provided connection details.
20
+ # You need to provide authentication details, and the user must have +edit+
21
+ # permissions on the blog if you want to read Post Meta, or to modify
22
+ # anything.
23
+ #
24
+ # @example
25
+ # client = WordpressClient.new(
26
+ # url: "https://blog.example.com/wp-json",
27
+ # username: "bot",
28
+ # password: ENV.fetch("WORDPRESS_PASSWORD"),
29
+ # )
30
+ #
31
+ # @see Client Client, for the methods available after creating the connection.
32
+ #
33
+ # @param url [String] The base URL to the wordpress install, including
34
+ # +/wp-json+.
35
+ # @param username [String] A valid username on the wordpress installation.
36
+ # @param password [String] The password for the provided user.
37
+ # @return {Client}
38
+ def self.new(url:, username:, password:)
39
+ Client.new(Connection.new(url: url, username: username, password: password))
24
40
  end
25
41
  end