wordpress_client 0.0.1 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
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