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.
- 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
|