wordpress_client 0.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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.hound.yml +2 -0
  4. data/.rubocop.yml +1065 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +11 -0
  7. data/Guardfile +29 -0
  8. data/LICENSE +21 -0
  9. data/README.md +63 -0
  10. data/Rakefile +1 -0
  11. data/circle.yml +3 -0
  12. data/lib/wordpress_client.rb +25 -0
  13. data/lib/wordpress_client/category.rb +4 -0
  14. data/lib/wordpress_client/client.rb +142 -0
  15. data/lib/wordpress_client/connection.rb +186 -0
  16. data/lib/wordpress_client/errors.rb +10 -0
  17. data/lib/wordpress_client/media.rb +37 -0
  18. data/lib/wordpress_client/media_parser.rb +48 -0
  19. data/lib/wordpress_client/paginated_collection.rb +53 -0
  20. data/lib/wordpress_client/post.rb +62 -0
  21. data/lib/wordpress_client/post_parser.rb +113 -0
  22. data/lib/wordpress_client/replace_metadata.rb +81 -0
  23. data/lib/wordpress_client/replace_terms.rb +62 -0
  24. data/lib/wordpress_client/rest_parser.rb +17 -0
  25. data/lib/wordpress_client/tag.rb +4 -0
  26. data/lib/wordpress_client/term.rb +34 -0
  27. data/lib/wordpress_client/version.rb +3 -0
  28. data/spec/category_spec.rb +8 -0
  29. data/spec/client_spec.rb +411 -0
  30. data/spec/connection_spec.rb +270 -0
  31. data/spec/docker/Dockerfile +40 -0
  32. data/spec/docker/README.md +37 -0
  33. data/spec/docker/dbdump.sql.gz +0 -0
  34. data/spec/docker/htaccess +10 -0
  35. data/spec/docker/restore-dbdump.sh +13 -0
  36. data/spec/fixtures/category.json +1 -0
  37. data/spec/fixtures/image-media.json +1 -0
  38. data/spec/fixtures/invalid-post-id.json +1 -0
  39. data/spec/fixtures/post-with-forbidden-metadata.json +1 -0
  40. data/spec/fixtures/post-with-metadata.json +1 -0
  41. data/spec/fixtures/simple-post.json +1 -0
  42. data/spec/fixtures/tag.json +1 -0
  43. data/spec/fixtures/thoughtful.jpg +0 -0
  44. data/spec/fixtures/validation-error.json +1 -0
  45. data/spec/integration/attachments_crud_spec.rb +51 -0
  46. data/spec/integration/categories_spec.rb +60 -0
  47. data/spec/integration/category_assignment_spec.rb +29 -0
  48. data/spec/integration/posts_crud_spec.rb +118 -0
  49. data/spec/integration/posts_finding_spec.rb +86 -0
  50. data/spec/integration/posts_metadata_spec.rb +27 -0
  51. data/spec/integration/posts_with_attachments_spec.rb +21 -0
  52. data/spec/integration/tag_assignment_spec.rb +29 -0
  53. data/spec/integration/tags_spec.rb +36 -0
  54. data/spec/media_spec.rb +63 -0
  55. data/spec/paginated_collection_spec.rb +64 -0
  56. data/spec/post_spec.rb +114 -0
  57. data/spec/replace_metadata_spec.rb +56 -0
  58. data/spec/replace_terms_spec.rb +51 -0
  59. data/spec/shared_examples/term_examples.rb +37 -0
  60. data/spec/spec_helper.rb +28 -0
  61. data/spec/support/docker_runner.rb +49 -0
  62. data/spec/support/fixtures.rb +19 -0
  63. data/spec/support/integration_macros.rb +10 -0
  64. data/spec/support/wordpress_server.rb +103 -0
  65. data/spec/tag_spec.rb +8 -0
  66. data/wordpress_client.gemspec +27 -0
  67. metadata +219 -0
@@ -0,0 +1,10 @@
1
+ module WordpressClient
2
+ Error = Class.new(::StandardError)
3
+
4
+ UnauthorizedError = Class.new(Error)
5
+
6
+ TimeoutError = Class.new(Error)
7
+ ServerError = Class.new(Error)
8
+ NotFoundError = Class.new(Error)
9
+ ValidationError = Class.new(Error)
10
+ end
@@ -0,0 +1,37 @@
1
+ module WordpressClient
2
+ class Media
3
+ attr_accessor(
4
+ :id, :slug, :title_html, :description,
5
+ :date, :updated_at,
6
+ :guid, :link, :media_details
7
+ )
8
+
9
+ def self.parse(data)
10
+ MediaParser.parse(data)
11
+ end
12
+
13
+ def initialize(
14
+ id: nil,
15
+ slug: nil,
16
+ title_html: nil,
17
+ description: nil,
18
+ date: nil,
19
+ updated_at: nil,
20
+ guid: nil,
21
+ link: nil,
22
+ media_details: {}
23
+ )
24
+ @id = id
25
+ @slug = slug
26
+ @title_html = title_html
27
+ @date = date
28
+ @updated_at = updated_at
29
+ @description = description
30
+ @guid = guid
31
+ @link = link
32
+ @media_details = media_details
33
+ end
34
+
35
+ alias source_url guid
36
+ end
37
+ end
@@ -0,0 +1,48 @@
1
+ module WordpressClient
2
+ class MediaParser
3
+ include RestParser
4
+
5
+ def self.parse(data)
6
+ new(data).to_media
7
+ end
8
+
9
+ def initialize(data)
10
+ @data = data
11
+ end
12
+
13
+ def to_media
14
+ media = Media.new
15
+
16
+ assign_basic(media)
17
+ assign_dates(media)
18
+ assign_rendered(media)
19
+ assign_guid(media)
20
+
21
+ media
22
+ end
23
+
24
+ private
25
+ attr_reader :data
26
+
27
+ def assign_basic(media)
28
+ media.id = data.fetch("id")
29
+ media.slug = data.fetch("slug")
30
+ media.link = data.fetch("link")
31
+ media.description = data["description"]
32
+ media.media_details = data["media_details"]
33
+ end
34
+
35
+ def assign_dates(media)
36
+ media.date = read_date("date")
37
+ media.updated_at = read_date("modified")
38
+ end
39
+
40
+ def assign_rendered(media)
41
+ media.title_html = rendered("title")
42
+ end
43
+
44
+ def assign_guid(media)
45
+ media.guid = rendered("guid") || data["source_url"]
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,53 @@
1
+ require "delegate"
2
+
3
+ module WordpressClient
4
+ class PaginatedCollection < DelegateClass(Array)
5
+ attr_reader :total, :current_page, :per_page
6
+
7
+ def initialize(entries, total:, current_page:, per_page:)
8
+ super(entries)
9
+ @total = total
10
+ @current_page = current_page
11
+ @per_page = per_page
12
+ end
13
+
14
+ #
15
+ # Pagination methods. Fulfilling will_paginate protocol
16
+ #
17
+
18
+ alias total_entries total
19
+
20
+ def total_pages
21
+ if total.zero? || per_page.zero?
22
+ 0
23
+ else
24
+ (total / per_page.to_f).ceil
25
+ end
26
+ end
27
+
28
+ def next_page
29
+ if current_page < total_pages
30
+ current_page + 1
31
+ end
32
+ end
33
+
34
+ def previous_page
35
+ if current_page > 1
36
+ current_page - 1
37
+ end
38
+ end
39
+
40
+ def out_of_bounds?
41
+ current_page < 1 || current_page > total_pages
42
+ end
43
+
44
+ # will_paginate < 3.0 has this method
45
+ def offset
46
+ if current_page > 0
47
+ (current_page - 1) * per_page
48
+ else
49
+ 0
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,62 @@
1
+ require "time"
2
+
3
+ module WordpressClient
4
+ class Post
5
+ attr_accessor(
6
+ :id, :slug, :url, :guid, :status,
7
+ :title_html, :excerpt_html, :content_html,
8
+ :updated_at, :date,
9
+ :categories, :tags, :meta, :featured_image
10
+ )
11
+
12
+ def self.parse(data)
13
+ PostParser.parse(data)
14
+ end
15
+
16
+ def initialize(
17
+ id: nil,
18
+ slug: nil,
19
+ url: nil,
20
+ guid: nil,
21
+ status: "unknown",
22
+ title_html: nil,
23
+ excerpt_html: nil,
24
+ content_html: nil,
25
+ updated_at: nil,
26
+ date: nil,
27
+ categories: [],
28
+ tags: [],
29
+ featured_image: nil,
30
+ meta: {},
31
+ meta_ids: {}
32
+ )
33
+ @id = id
34
+ @slug = slug
35
+ @url = url
36
+ @guid = guid
37
+ @status = status
38
+ @title_html = title_html
39
+ @excerpt_html = excerpt_html
40
+ @content_html = content_html
41
+ @updated_at = updated_at
42
+ @date = date
43
+ @categories = categories
44
+ @tags = tags
45
+ @featured_image = featured_image
46
+ @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
+ end
57
+
58
+ def meta_id_for(key)
59
+ @meta_ids[key] || raise(ArgumentError, "Post does not have meta #{key.inspect}")
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,113 @@
1
+ module WordpressClient
2
+ class PostParser
3
+ include RestParser
4
+
5
+ def self.parse(data)
6
+ new(data).to_post
7
+ end
8
+
9
+ def initialize(data)
10
+ @data = data
11
+ @embedded = data.fetch("_embedded", {})
12
+ end
13
+
14
+ def to_post
15
+ meta, meta_ids = parse_metadata
16
+ post = Post.new(meta: meta, meta_ids: meta_ids)
17
+
18
+ assign_basic(post)
19
+ assign_dates(post)
20
+ assign_rendered(post)
21
+ assign_categories(post)
22
+ assign_tags(post)
23
+ assign_featured_image(post)
24
+
25
+ post
26
+ end
27
+
28
+ private
29
+ attr_reader :data, :embedded
30
+
31
+ def assign_basic(post)
32
+ post.id = data["id"]
33
+ post.slug = data["slug"]
34
+ post.url = data["link"]
35
+ post.status = data["status"]
36
+ end
37
+
38
+ def assign_dates(post)
39
+ post.updated_at = read_date("modified")
40
+ post.date = read_date("date")
41
+ end
42
+
43
+ def assign_rendered(post)
44
+ post.guid = rendered("guid")
45
+ post.title_html = rendered("title")
46
+ post.excerpt_html = rendered("excerpt")
47
+ post.content_html = rendered("content")
48
+ end
49
+
50
+ def assign_categories(post)
51
+ post.categories = embedded_terms("category").map do |category|
52
+ Category.parse(category)
53
+ end
54
+ end
55
+
56
+ def assign_tags(post)
57
+ post.tags = embedded_terms("post_tag").map do |tag|
58
+ Tag.parse(tag)
59
+ end
60
+ end
61
+
62
+ def assign_featured_image(post)
63
+ featured_id = data["featured_image"]
64
+ if featured_id
65
+ features = (embedded["https://api.w.org/featuredmedia"] || []).flatten
66
+ media = features.detect { |feature| feature["id"] == featured_id }
67
+ if media
68
+ post.featured_image = Media.parse(media)
69
+ end
70
+ end
71
+ end
72
+
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
+ def embedded_terms(type)
89
+ term_collections = embedded["https://api.w.org/term"] || []
90
+
91
+ # term_collections is an array of arrays with terms in them. We can see
92
+ # the type of the "collection" by inspecting the first child's taxonomy.
93
+ term_collections.detect { |terms|
94
+ terms.size > 0 && terms.is_a?(Array) && terms.first["taxonomy"] == type
95
+ } || []
96
+ end
97
+
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
+ end
113
+ end
@@ -0,0 +1,81 @@
1
+ require "set"
2
+
3
+ module WordpressClient
4
+ class ReplaceMetadata
5
+ def self.apply(connection, post, meta)
6
+ instance = new(connection, post, meta)
7
+ instance.apply
8
+ instance.number_of_changes
9
+ end
10
+
11
+ attr_reader :number_of_changes
12
+
13
+ def initialize(connection, post, meta)
14
+ @connection = connection
15
+ @post = post
16
+ @existing_meta = post.meta
17
+ @new_meta = stringify_keys(meta)
18
+ @number_of_changes = 0
19
+ end
20
+
21
+ def apply
22
+ all_keys.each do |key|
23
+ action = determine_action(key)
24
+ send(action, key, new_meta[key])
25
+ end
26
+ end
27
+
28
+ private
29
+ attr_reader :connection, :post, :new_meta, :existing_meta
30
+
31
+ def meta_id(key)
32
+ post.meta_id_for(key)
33
+ end
34
+
35
+ def all_keys
36
+ (new_meta.keys + existing_meta.keys).to_set
37
+ end
38
+
39
+ def determine_action(key)
40
+ old_value = existing_meta[key]
41
+ new_value = new_meta[key]
42
+
43
+ if old_value.nil? && !new_value.nil?
44
+ :add
45
+ elsif old_value == new_value
46
+ :keep
47
+ elsif new_value.nil?
48
+ :remove
49
+ else
50
+ :replace
51
+ end
52
+ end
53
+
54
+ def add(key, value)
55
+ connection.create_without_response("posts/#{post.id}/meta", key: key, value: value)
56
+ @number_of_changes += 1
57
+ end
58
+
59
+ def remove(key, *)
60
+ connection.delete("posts/#{post.id}/meta/#{meta_id(key)}", force: true)
61
+ @number_of_changes += 1
62
+ end
63
+
64
+ def replace(key, value)
65
+ connection.patch_without_response(
66
+ "posts/#{post.id}/meta/#{meta_id(key)}", key: key, value: value
67
+ )
68
+ @number_of_changes += 1
69
+ end
70
+
71
+ def keep(*)
72
+ # Do nothing. This method is here to satisfy every action of #determine_action.
73
+ end
74
+
75
+ def stringify_keys(hash)
76
+ hash.each_with_object({}) do |(key, value), new_hash|
77
+ new_hash[key.to_s] = value
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,62 @@
1
+ require "set"
2
+
3
+ module WordpressClient
4
+ class ReplaceTerms
5
+ def self.apply_categories(connection, post, category_ids)
6
+ instance = new(
7
+ connection,
8
+ post.id,
9
+ post.category_ids,
10
+ category_ids
11
+ )
12
+ instance.replace("category")
13
+ instance.number_of_changes
14
+ end
15
+
16
+ def self.apply_tags(connection, post, tag_ids)
17
+ instance = new(
18
+ connection,
19
+ post.id,
20
+ post.tag_ids,
21
+ tag_ids
22
+ )
23
+ instance.replace("tag")
24
+ instance.number_of_changes
25
+ end
26
+
27
+ def initialize(connection, post_id, existing_ids, new_ids)
28
+ @connection = connection
29
+ @post_id = post_id
30
+ @existing_ids = existing_ids.to_set
31
+ @wanted_ids = new_ids.to_set
32
+ end
33
+
34
+ def replace(type)
35
+ ids_to_add.each { |id| add_term_id(id, type) }
36
+ ids_to_remove.each { |id| remove_term_id(id, type) }
37
+ end
38
+
39
+ def number_of_changes
40
+ ids_to_add.size + ids_to_remove.size
41
+ end
42
+
43
+ private
44
+ attr_reader :connection, :post_id, :wanted_ids, :existing_ids
45
+
46
+ def ids_to_add
47
+ wanted_ids - existing_ids
48
+ end
49
+
50
+ def ids_to_remove
51
+ existing_ids - wanted_ids
52
+ end
53
+
54
+ def add_term_id(id, type)
55
+ connection.create_without_response("posts/#{post_id}/terms/#{type}/#{id}", {})
56
+ end
57
+
58
+ def remove_term_id(id, type)
59
+ connection.delete("posts/#{post_id}/terms/#{type}/#{id}", force: true)
60
+ end
61
+ end
62
+ end