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
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.2.2
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in wordpress_client.gemspec
4
+ gemspec
5
+
6
+ gem "codeclimate-test-reporter", group: :test, require: nil
7
+
8
+ group :development do
9
+ gem 'guard', '~> 2.13'
10
+ gem 'guard-rspec', '~> 4.6'
11
+ end
data/Guardfile ADDED
@@ -0,0 +1,29 @@
1
+ rspec_options = {
2
+ cmd: 'rspec -f documentation -t focus',
3
+ failed_mode: :keep,
4
+ all_after_pass: true,
5
+ all_on_start: true,
6
+ run_all: {cmd: 'rspec -f progress -t focus'}
7
+ }
8
+
9
+ guard :rspec, rspec_options do
10
+ require "guard/rspec/dsl"
11
+ dsl = Guard::RSpec::Dsl.new(self)
12
+
13
+ # RSpec files
14
+ rspec = dsl.rspec
15
+ watch(rspec.spec_helper) { rspec.spec_dir }
16
+ watch(rspec.spec_support) { rspec.spec_dir }
17
+ watch(rspec.spec_files)
18
+
19
+ watch("spec/shared_examples/term_examples.rb") do
20
+ [
21
+ "spec/category_spec.rb",
22
+ "spec/tag_spec.rb",
23
+ ]
24
+ end
25
+
26
+ # Ruby files
27
+ watch("lib/wordpress_client.rb") { rspec.spec_dir }
28
+ dsl.watch_spec_files_for(%r{^lib/wordpress_client/(.*)\.rb$})
29
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Hemnet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # WordpressClient
2
+
3
+ WordpressClient is a very simple client to the Wordpress API, version 2 beta 8.0.
4
+
5
+ **NOTE:** The repository is still named `wpclient` as we're in the middle of a rename. Some references might persist until it's completed.
6
+
7
+ [![Circle CI](https://circleci.com/gh/hemnet/wpclient.svg?style=svg)](https://circleci.com/gh/hemnet/wpclient) [![Code Climate](https://codeclimate.com/repos/5645938269568041da00cded/badges/5e870b57428f23c1f2ff/gpa.svg)](https://codeclimate.com/repos/5645938269568041da00cded/feed) [![Test Coverage](https://codeclimate.com/repos/5645938269568041da00cded/badges/5e870b57428f23c1f2ff/coverage.svg)](https://codeclimate.com/repos/5645938269568041da00cded/coverage)
8
+
9
+ ## Usage
10
+
11
+ Initialize a client with a username, password and API URL. You can then search for posts.
12
+
13
+ ```ruby
14
+ client = WordpressClient.new(url: "https://example.com/wp-json/", username: "example", password: "example")
15
+
16
+ client.posts(per_page: 5) # => [WordpressClient::Post, WordpressClient::Post]
17
+ ```
18
+
19
+ ### Creating a post
20
+
21
+ You can create posts by calling `create_post`. If you supply a ID, the article will be created using `PUT` instead of `POST`.
22
+
23
+ ```ruby
24
+ data = {
25
+ author: "Name",
26
+ # ...
27
+ }
28
+
29
+ post = client.create_post(data) # => WordpressClient::Post
30
+ updated_post = client.update_post(post.id, title: "Updated") # => WordpressClient::Post
31
+
32
+ updated_post.title_html # => "Updated"
33
+ ```
34
+
35
+ ## Running tests
36
+
37
+ You need to install Docker and set it up for your machine. Note that you need `docker-machine` to run Docker on OS X.
38
+
39
+ Run tests using the normal `rspec` command after installing all bundles. The first time the integration tests are run, a docker image will be built that hosts a Wordpress installation, but the image will be re-used on subsequent runs.
40
+
41
+ ```
42
+ bundle exec rspec
43
+ ```
44
+
45
+ You can also run `bundle exec guard` to have tests run automatically when you change files in the repo. If you tag your examples with `focus: true`, Guard will only run those tests. This can help when doing very focused coding, but remember to remove the filter before you commit and let the entire suite run.
46
+
47
+ ```ruby
48
+ describe Foo, focus: true do
49
+ # ...
50
+ end
51
+ ```
52
+
53
+ The normal `rspec` command will *not* use this filter in case it is ever committed accidentally, so CI can catch any problems.
54
+
55
+ ## Copyright & License
56
+
57
+ Copyright © 2015 Hemnet Service HNS AB
58
+
59
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
60
+
61
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
62
+
63
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/circle.yml ADDED
@@ -0,0 +1,3 @@
1
+ machine:
2
+ services:
3
+ - docker
@@ -0,0 +1,25 @@
1
+ require "wordpress_client/version"
2
+ require "wordpress_client/errors"
3
+
4
+ require "wordpress_client/rest_parser"
5
+ require "wordpress_client/connection"
6
+ require "wordpress_client/client"
7
+ require "wordpress_client/paginated_collection"
8
+
9
+ require "wordpress_client/term"
10
+ require "wordpress_client/category"
11
+ require "wordpress_client/tag"
12
+
13
+ require "wordpress_client/post"
14
+ require "wordpress_client/post_parser"
15
+ require "wordpress_client/media"
16
+ require "wordpress_client/media_parser"
17
+
18
+ require "wordpress_client/replace_terms"
19
+ require "wordpress_client/replace_metadata"
20
+
21
+ module WordpressClient
22
+ def self.new(*args)
23
+ Client.new(Connection.new(*args))
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ module WordpressClient
2
+ class Category < Term
3
+ end
4
+ end
@@ -0,0 +1,142 @@
1
+ module WordpressClient
2
+ class Client
3
+ def initialize(connection)
4
+ @connection = connection
5
+ end
6
+
7
+ def posts(per_page: 10, page: 1, category_slug: nil, tag_slug: nil)
8
+ filter = {}
9
+ filter[:category_name] = category_slug if category_slug
10
+ filter[:tag] = tag_slug if tag_slug
11
+ connection.get_multiple(
12
+ Post, "posts", per_page: per_page, page: page, _embed: nil, context: "edit", filter: filter
13
+ )
14
+ end
15
+
16
+ def categories(per_page: 10, page: 1)
17
+ connection.get_multiple(Category, "terms/category", page: page, per_page: per_page)
18
+ end
19
+
20
+ def tags(per_page: 10, page: 1)
21
+ connection.get_multiple(Tag, "terms/tag", page: page, per_page: per_page)
22
+ end
23
+
24
+ def media(per_page: 10, page: 1)
25
+ connection.get_multiple(Media, "media", page: page, per_page: per_page)
26
+ end
27
+
28
+ def find_post(id)
29
+ connection.get(Post, "posts/#{id.to_i}", _embed: nil, context: "edit")
30
+ end
31
+
32
+ def find_by_slug(slug)
33
+ posts = connection.get_multiple(
34
+ Post, "posts", per_page: 1, page: 1, filter: {name: slug}, _embed: nil
35
+ )
36
+ if posts.size > 0
37
+ posts.first
38
+ else
39
+ raise NotFoundError, "Could not find post with slug #{slug.to_s.inspect}"
40
+ end
41
+ end
42
+
43
+ def find_category(id)
44
+ connection.get(Category, "terms/category/#{id.to_i}")
45
+ end
46
+
47
+ def find_tag(id)
48
+ connection.get(Tag, "terms/tag/#{id.to_i}")
49
+ end
50
+
51
+ def find_media(id)
52
+ connection.get(Media, "media/#{id.to_i}")
53
+ end
54
+
55
+ def create_post(attributes)
56
+ post = connection.create(Post, "posts", attributes, redirect_params: {_embed: nil})
57
+
58
+ changes = 0
59
+ changes += assign_meta(post, attributes[:meta])
60
+ changes += assign_categories(post, attributes[:category_ids])
61
+ changes += assign_tags(post, attributes[:tag_ids])
62
+
63
+ if changes > 0
64
+ find_post(post.id)
65
+ else
66
+ post
67
+ end
68
+ end
69
+
70
+ def create_category(attributes)
71
+ connection.create(Category, "terms/category", attributes)
72
+ end
73
+
74
+ def create_tag(attributes)
75
+ connection.create(Tag, "terms/tag", attributes)
76
+ end
77
+
78
+ def update_post(id, attributes)
79
+ post = connection.patch(Post, "posts/#{id.to_i}?_embed", attributes)
80
+
81
+ changes = 0
82
+ changes += assign_meta(post, attributes[:meta])
83
+ changes += assign_categories(post, attributes[:category_ids])
84
+ changes += assign_tags(post, attributes[:tag_ids])
85
+
86
+ if changes > 0
87
+ find_post(post.id)
88
+ else
89
+ post
90
+ end
91
+ end
92
+
93
+ def update_category(id, attributes)
94
+ connection.patch(Category, "terms/category/#{id.to_i}", attributes)
95
+ end
96
+
97
+ def update_tag(id, attributes)
98
+ connection.patch(Tag, "terms/tag/#{id.to_i}", attributes)
99
+ end
100
+
101
+ def update_media(id, attributes)
102
+ connection.patch(Media, "media/#{id.to_i}", attributes)
103
+ end
104
+
105
+ def upload(io, mime_type:, filename:)
106
+ connection.upload(Media, "media", io, mime_type: mime_type, filename: filename)
107
+ end
108
+
109
+ def upload_file(filename, mime_type:)
110
+ path = filename.to_s
111
+ File.open(path, 'r') do |file|
112
+ upload(file, mime_type: mime_type, filename: File.basename(path))
113
+ end
114
+ end
115
+
116
+ def delete_post(id, force: false)
117
+ connection.delete("posts/#{id.to_i}", {"force" => force})
118
+ end
119
+
120
+ def inspect
121
+ "#<WordpressClient::Client #{connection.inspect}>"
122
+ end
123
+
124
+ private
125
+ attr_reader :connection
126
+
127
+ def assign_categories(post, ids)
128
+ return 0 unless ids
129
+ ReplaceTerms.apply_categories(connection, post, ids)
130
+ end
131
+
132
+ def assign_tags(post, ids)
133
+ return 0 unless ids
134
+ ReplaceTerms.apply_tags(connection, post, ids)
135
+ end
136
+
137
+ def assign_meta(post, meta)
138
+ return 0 unless meta
139
+ ReplaceMetadata.apply(connection, post, meta)
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,186 @@
1
+ require "faraday"
2
+ require "json"
3
+
4
+ module WordpressClient
5
+ class Connection
6
+ attr_reader :url, :username
7
+
8
+ def initialize(url:, username:, password:)
9
+ @url = url
10
+ @username = username
11
+ @password = password
12
+ end
13
+
14
+ def get(model, path, params = {})
15
+ model.parse(get_json(path, params))
16
+ end
17
+
18
+ def get_multiple(model, path, params = {})
19
+ data, response = get_json_and_response(path, params)
20
+ models = data.map { |model_data| model.parse(model_data) }
21
+ wrap_paginated_collection(response, models, params)
22
+ end
23
+
24
+ def create(model, path, attributes, redirect_params: {})
25
+ response = send_json(path, attributes)
26
+
27
+ if response.status == 201 # Created
28
+ model.parse(get_json(response.headers.fetch("location"), redirect_params))
29
+ else
30
+ handle_status_code(response)
31
+ model.parse(parse_json_response(response))
32
+ end
33
+ end
34
+
35
+ def create_without_response(path, attributes)
36
+ response = send_json(path, attributes)
37
+
38
+ if response.status == 201 # Created
39
+ true
40
+ else
41
+ handle_status_code(response)
42
+ true
43
+ end
44
+ end
45
+
46
+ def delete(path, attributes = {})
47
+ response = send_json(path, attributes, method: :delete)
48
+ handle_status_code(response)
49
+ true
50
+ end
51
+
52
+ def patch(model, path, attributes)
53
+ model.parse(
54
+ parse_json_response(send_json(path, attributes, method: :patch))
55
+ )
56
+ end
57
+
58
+ def patch_without_response(path, attributes)
59
+ handle_status_code(send_json(path, attributes, method: :patch))
60
+ true
61
+ end
62
+
63
+ def upload(model, path, io, mime_type:, filename:)
64
+ body = io.read
65
+ response = post_data(path, body, {
66
+ "Content-Length" => body.size.to_s,
67
+ "Content-Type" => mime_type,
68
+ # WP API does not parse normal Content-Disposition and instead ops to using their own format
69
+ # https://github.com/WP-API/WP-API/issues/1744
70
+ "Content-Disposition" => "filename=#{filename || "unnamed"}",
71
+ })
72
+
73
+ if response.status == 201 # Created
74
+ model.parse(get_json(response.headers.fetch("location")))
75
+ else
76
+ handle_status_code(response)
77
+ model.parse(parse_json_response(response))
78
+ end
79
+ end
80
+
81
+ def inspect
82
+ "#<#{self.class.name} #@username @ #@url>"
83
+ end
84
+
85
+ private
86
+ def net
87
+ @net ||= setup_network_connection
88
+ end
89
+
90
+ def setup_network_connection
91
+ Faraday.new(url: "#{url}/wp/v2") do |conn|
92
+ conn.request :basic_auth, username, @password
93
+ conn.adapter :net_http
94
+ end
95
+ end
96
+
97
+ def wrap_paginated_collection(response, entries, params)
98
+ total = response.headers.fetch("x-wp-total").to_i
99
+ current_page = params.fetch(:page).to_i
100
+ per_page = params.fetch(:per_page).to_i
101
+
102
+ PaginatedCollection.new(
103
+ entries, total: total, current_page: current_page, per_page: per_page
104
+ )
105
+ end
106
+
107
+ def get_json(path, params = {})
108
+ get_json_and_response(path, params).first
109
+ end
110
+
111
+ def get_json_and_response(path, params = {})
112
+ response = net.get(path, params)
113
+ [parse_json_response(response), response]
114
+ rescue Faraday::TimeoutError
115
+ raise TimeoutError
116
+ end
117
+
118
+ def send_json(path, data, method: :post)
119
+ unless %i[get post put patch delete].include? method
120
+ raise ArgumentError, "Invalid method: #{method.inspect}"
121
+ end
122
+
123
+ net.public_send(method) do |request|
124
+ json = data.to_json
125
+ request.url path
126
+ request.headers["Content-Type"] = "application/json; charset=#{json.encoding}"
127
+ request.body = json
128
+ end
129
+ rescue Faraday::TimeoutError
130
+ raise TimeoutError
131
+ end
132
+
133
+ def post_data(path, data, headers)
134
+ net.post do |request|
135
+ request.url path
136
+ request.headers = headers
137
+ request.body = data
138
+ end
139
+ rescue Faraday::TimeoutError
140
+ raise TimeoutError
141
+ end
142
+
143
+ def parse_json_response(response)
144
+ handle_status_code(response)
145
+
146
+ content_type = response.headers["content-type"].split(";").first
147
+ unless content_type == "application/json"
148
+ raise ServerError, "Got content type #{content_type}"
149
+ end
150
+
151
+ JSON.parse(response.body)
152
+
153
+ rescue JSON::ParserError => error
154
+ raise ServerError, "Could not parse JSON response: #{error}"
155
+ end
156
+
157
+ def handle_status_code(response)
158
+ case response.status
159
+ when 200
160
+ return
161
+ when 404
162
+ raise NotFoundError, "Could not find resource"
163
+ when 400
164
+ handle_bad_request(response)
165
+ else
166
+ raise ServerError, "Server returned status code #{response.status}: #{response.body}"
167
+ end
168
+ end
169
+
170
+ def handle_bad_request(response)
171
+ code, message = bad_request_details(response)
172
+ if code == "rest_post_invalid_id"
173
+ raise NotFoundError, "Post ID is not found"
174
+ else
175
+ raise ValidationError, message
176
+ end
177
+ end
178
+
179
+ def bad_request_details(response)
180
+ details = JSON.parse(response.body)
181
+ [details["code"], details["message"]]
182
+ rescue
183
+ [nil, "Bad Request"]
184
+ end
185
+ end
186
+ end