wordpress_client 0.0.1

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