wordpress_client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.hound.yml +2 -0
- data/.rubocop.yml +1065 -0
- data/.ruby-version +1 -0
- data/Gemfile +11 -0
- data/Guardfile +29 -0
- data/LICENSE +21 -0
- data/README.md +63 -0
- data/Rakefile +1 -0
- data/circle.yml +3 -0
- data/lib/wordpress_client.rb +25 -0
- data/lib/wordpress_client/category.rb +4 -0
- data/lib/wordpress_client/client.rb +142 -0
- data/lib/wordpress_client/connection.rb +186 -0
- data/lib/wordpress_client/errors.rb +10 -0
- data/lib/wordpress_client/media.rb +37 -0
- data/lib/wordpress_client/media_parser.rb +48 -0
- data/lib/wordpress_client/paginated_collection.rb +53 -0
- data/lib/wordpress_client/post.rb +62 -0
- data/lib/wordpress_client/post_parser.rb +113 -0
- data/lib/wordpress_client/replace_metadata.rb +81 -0
- data/lib/wordpress_client/replace_terms.rb +62 -0
- data/lib/wordpress_client/rest_parser.rb +17 -0
- data/lib/wordpress_client/tag.rb +4 -0
- data/lib/wordpress_client/term.rb +34 -0
- data/lib/wordpress_client/version.rb +3 -0
- data/spec/category_spec.rb +8 -0
- data/spec/client_spec.rb +411 -0
- data/spec/connection_spec.rb +270 -0
- data/spec/docker/Dockerfile +40 -0
- data/spec/docker/README.md +37 -0
- data/spec/docker/dbdump.sql.gz +0 -0
- data/spec/docker/htaccess +10 -0
- data/spec/docker/restore-dbdump.sh +13 -0
- data/spec/fixtures/category.json +1 -0
- data/spec/fixtures/image-media.json +1 -0
- data/spec/fixtures/invalid-post-id.json +1 -0
- data/spec/fixtures/post-with-forbidden-metadata.json +1 -0
- data/spec/fixtures/post-with-metadata.json +1 -0
- data/spec/fixtures/simple-post.json +1 -0
- data/spec/fixtures/tag.json +1 -0
- data/spec/fixtures/thoughtful.jpg +0 -0
- data/spec/fixtures/validation-error.json +1 -0
- data/spec/integration/attachments_crud_spec.rb +51 -0
- data/spec/integration/categories_spec.rb +60 -0
- data/spec/integration/category_assignment_spec.rb +29 -0
- data/spec/integration/posts_crud_spec.rb +118 -0
- data/spec/integration/posts_finding_spec.rb +86 -0
- data/spec/integration/posts_metadata_spec.rb +27 -0
- data/spec/integration/posts_with_attachments_spec.rb +21 -0
- data/spec/integration/tag_assignment_spec.rb +29 -0
- data/spec/integration/tags_spec.rb +36 -0
- data/spec/media_spec.rb +63 -0
- data/spec/paginated_collection_spec.rb +64 -0
- data/spec/post_spec.rb +114 -0
- data/spec/replace_metadata_spec.rb +56 -0
- data/spec/replace_terms_spec.rb +51 -0
- data/spec/shared_examples/term_examples.rb +37 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/docker_runner.rb +49 -0
- data/spec/support/fixtures.rb +19 -0
- data/spec/support/integration_macros.rb +10 -0
- data/spec/support/wordpress_server.rb +103 -0
- data/spec/tag_spec.rb +8 -0
- data/wordpress_client.gemspec +27 -0
- metadata +219 -0
@@ -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
|