wikidatum 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,368 +3,436 @@
3
3
  require 'faraday'
4
4
  require 'faraday/net_http'
5
5
 
6
- class Wikidatum::Client
7
- ITEM_REGEX = /^Q?\d+$/.freeze
8
- STATEMENT_REGEX = /^Q?\d+\$[\w-]+$/.freeze
9
-
10
- # @return [String] the root URL of the Wikibase instance we want to interact
11
- # with. If not provided, will default to Wikidata.
12
- attr_reader :wikibase_url
13
-
14
- # @return [Boolean] whether this client instance should identify itself
15
- # as a bot when making requests.
16
- attr_reader :bot
17
-
18
- # @return [String] the UserAgent header to send with all requests to the
19
- # Wikibase API.
20
- attr_reader :user_agent
21
-
22
- # Create a new Wikidatum::Client to interact with the Wikibase REST API.
23
- #
24
- # @example
25
- # wikidatum_client = Wikidatum::Client.new(
26
- # user_agent: 'REPLACE ME WITH THE NAME OF YOUR BOT!',
27
- # wikibase_url: 'https://www.wikidata.org',
28
- # bot: true
29
- # )
30
- #
31
- # @param user_agent [String] The UserAgent header to send with all requests
32
- # to the Wikibase API.
33
- # @param wikibase_url [String] The root URL of the Wikibase instance we want
34
- # to interact with. If not provided, will default to
35
- # `https://www.wikidata.org`. Do not include a `/` at the end of the URL.
36
- # @param bot [Boolean] Whether requests sent by this client instance should
37
- # be registered as bot requests. Defaults to `true`.
38
- # @return [Wikidatum::Client]
39
- def initialize(user_agent:, wikibase_url: 'https://www.wikidata.org', bot: true)
40
- raise ArgumentError, "Wikibase URL must not end with a `/`, got #{wikibase_url.inspect}." if wikibase_url.end_with?('/')
41
-
42
- # TODO: Add the Ruby gem version to the UserAgent automatically, and
43
- # restrict the ability for end-users to actually set the UserAgent?
44
- @user_agent = user_agent
45
- @wikibase_url = wikibase_url
46
- @bot = bot
47
-
48
- Faraday.default_adapter = :net_http
49
- end
6
+ module Wikidatum
7
+ class Client
8
+ ITEM_REGEX = /^Q?\d+$/.freeze
9
+ PROPERTY_REGEX = /^P?\d+$/.freeze
10
+ STATEMENT_REGEX = /^Q?\d+\$[\w-]+$/.freeze
11
+ VALID_RANKS = ['preferred', 'normal', 'deprecated'].freeze
12
+ VALID_DATA_TYPES = [
13
+ 'Wikidatum::DataType::GlobeCoordinate',
14
+ 'Wikidatum::DataType::MonolingualText',
15
+ 'Wikidatum::DataType::NoValue',
16
+ 'Wikidatum::DataType::Quantity',
17
+ 'Wikidatum::DataType::SomeValue',
18
+ 'Wikidatum::DataType::Time',
19
+ 'Wikidatum::DataType::WikibaseItem',
20
+ 'Wikidatum::DataType::WikibaseString'
21
+ ].freeze
22
+
23
+ # @return [String] the root URL of the Wikibase instance we want to interact
24
+ # with. If not provided, will default to Wikidata.
25
+ attr_reader :wikibase_url
26
+
27
+ # @return [Boolean] whether this client instance should identify itself
28
+ # as a bot when making requests.
29
+ attr_reader :bot
30
+
31
+ # @return [String] the UserAgent header to send with all requests to the
32
+ # Wikibase API.
33
+ attr_reader :user_agent
34
+
35
+ # @return [Boolean] whether this client should allow non-GET requests if
36
+ # authentication hasn't been provided. Defaults to false.
37
+ attr_reader :allow_ip_edits
38
+
39
+ # Create a new Wikidatum::Client to interact with the Wikibase REST API.
40
+ #
41
+ # @example
42
+ # wikidatum_client = Wikidatum::Client.new(
43
+ # user_agent: 'Bot Name',
44
+ # wikibase_url: 'https://www.wikidata.org',
45
+ # bot: true
46
+ # )
47
+ #
48
+ # @param user_agent [String] The UserAgent header to send with all requests
49
+ # to the Wikibase API. This will be prepended with the string "Wikidatum
50
+ # Ruby gem vX.X.X:".
51
+ # @param wikibase_url [String] The root URL of the Wikibase instance we want
52
+ # to interact with. If not provided, will default to
53
+ # `https://www.wikidata.org`. Do not include a `/` at the end of the URL.
54
+ # @param bot [Boolean] Whether requests sent by this client instance should
55
+ # be registered as bot requests. Defaults to `true`.
56
+ # @param allow_ip_edits [Boolean] whether this client should allow non-GET
57
+ # requests if authentication hasn't been provided. Defaults to false. If
58
+ # this is set to true, the IP address of the device from which the
59
+ # request was sent will be credited for the edit. Make sure not to allow
60
+ # these edits if you don't want your IP address (and in many cases, a
61
+ # very close approximation of your physical location) exposed publicly.
62
+ # @return [Wikidatum::Client]
63
+ def initialize(user_agent:, wikibase_url: 'https://www.wikidata.org', bot: true, allow_ip_edits: false)
64
+ raise ArgumentError, "Wikibase URL must not end with a `/`, got #{wikibase_url.inspect}." if wikibase_url.end_with?('/')
65
+
66
+ @user_agent = "Wikidatum Ruby gem v#{Wikidatum::VERSION}: #{user_agent}"
67
+ @wikibase_url = wikibase_url
68
+ @bot = bot
69
+ @allow_ip_edits = allow_ip_edits
70
+
71
+ Faraday.default_adapter = :net_http
72
+ end
50
73
 
51
- # Get an item from the Wikibase API based on its QID.
52
- #
53
- # @example
54
- # wikidatum_client.item(id: 'Q123')
55
- # wikidatum_client.item(id: 123)
56
- # wikidatum_client.item(id: '123')
57
- #
58
- # @param id [String, Integer] Either a string or integer representation of
59
- # the item's QID, e.g. `"Q123"`, `"123"`, or `123`.
60
- # @return [Wikidatum::Item]
61
- def item(id:)
62
- raise ArgumentError, "#{id.inspect} is an invalid Wikibase QID. Must be an integer, a string representation of an integer, or in the format 'Q123'." unless id.is_a?(Integer) || id.match?(ITEM_REGEX)
74
+ # Get an item from the Wikibase API based on its QID.
75
+ #
76
+ # @example
77
+ # wikidatum_client.item(id: 'Q123')
78
+ # wikidatum_client.item(id: 123)
79
+ # wikidatum_client.item(id: '123')
80
+ #
81
+ # @param id [String, Integer] Either a string or integer representation of
82
+ # the item's QID, e.g. `"Q123"`, `"123"`, or `123`.
83
+ # @return [Wikidatum::Item]
84
+ def item(id:)
85
+ raise ArgumentError, "#{id.inspect} is an invalid Wikibase QID. Must be an integer, a string representation of an integer, or in the format 'Q123'." unless id.is_a?(Integer) || id.match?(ITEM_REGEX)
63
86
 
64
- id = coerce_item_id(id)
87
+ id = coerce_item_id(id)
65
88
 
66
- response = get_request("/entities/items/#{id}")
89
+ response = get_request("/entities/items/#{id}")
67
90
 
68
- puts JSON.pretty_generate(response) if ENV['DEBUG']
91
+ puts JSON.pretty_generate(response) if ENV['DEBUG']
69
92
 
70
- Wikidatum::Item.marshal_load(response)
71
- end
93
+ Wikidatum::Item.marshal_load(response)
94
+ end
72
95
 
73
- # Get a statement from the Wikibase API based on its ID.
74
- #
75
- # @example
76
- # wikidatum_client.statement(id: 'Q123$f004ec2b-4857-3b69-b370-e8124f5bd3ac')
77
- #
78
- # @param id [String] A string representation of the statement's ID.
79
- # @return [Wikidatum::Statement]
80
- def statement(id:)
81
- raise ArgumentError, "#{id.inspect} is an invalid Wikibase Statement ID. Must be a string in the format 'Q123$f004ec2b-4857-3b69-b370-e8124f5bd3ac'." unless id.match?(STATEMENT_REGEX)
96
+ # Get a statement from the Wikibase API based on its ID.
97
+ #
98
+ # @example
99
+ # wikidatum_client.statement(id: 'Q123$f004ec2b-4857-3b69-b370-e8124f5bd3ac')
100
+ #
101
+ # @param id [String] A string representation of the statement's ID.
102
+ # @return [Wikidatum::Statement]
103
+ def statement(id:)
104
+ raise ArgumentError, "#{id.inspect} is an invalid Wikibase Statement ID. Must be a string in the format 'Q123$f004ec2b-4857-3b69-b370-e8124f5bd3ac'." unless id.match?(STATEMENT_REGEX)
82
105
 
83
- response = get_request("/statements/#{id}")
106
+ response = get_request("/statements/#{id}")
84
107
 
85
- puts JSON.pretty_generate(response) if ENV['DEBUG']
108
+ puts JSON.pretty_generate(response) if ENV['DEBUG']
86
109
 
87
- Wikidatum::Statement.marshal_load(response)
88
- end
110
+ Wikidatum::Statement.marshal_load(response)
111
+ end
89
112
 
90
- # Add a statement to an item.
91
- #
92
- # NOTE: Adding references/qualifiers with `add_statement` is untested and
93
- # effectively unsupported for now.
94
- #
95
- # @example Add a string statement.
96
- # wikidatum_client.add_statement(
97
- # id: 'Q123',
98
- # property: 'P23',
99
- # datavalue: Wikidatum::DataValueType::WikibaseString.new(string: 'Foo'),
100
- # comment: 'Adding something or another.'
101
- # )
102
- #
103
- # @example Add a 'no value' statement.
104
- # wikidatum_client.add_statement(
105
- # id: 'Q123',
106
- # property: 'P124',
107
- # datavalue: Wikidatum::DataValueType::NoValue.new(
108
- # type: :no_value,
109
- # value: nil
110
- # )
111
- # )
112
- #
113
- # @example Add an 'unknown value' statement.
114
- # wikidatum_client.add_statement(
115
- # id: 'Q123',
116
- # property: 'P124',
117
- # datavalue: Wikidatum::DataValueType::SomeValue.new(
118
- # type: :some_value,
119
- # value: nil
120
- # )
121
- # )
122
- #
123
- # @example Add a globe coordinate statement.
124
- # wikidatum_client.add_statement(
125
- # id: 'Q123',
126
- # property: 'P124',
127
- # datavalue: Wikidatum::DataValueType::GlobeCoordinate.new(
128
- # latitude: 52.51666,
129
- # longitude: 13.3833,
130
- # precision: 0.01666,
131
- # globe: 'https://wikidata.org/entity/Q2'
132
- # )
133
- # )
134
- #
135
- # @example Add a monolingual text statement.
136
- # wikidatum_client.add_statement(
137
- # id: 'Q123',
138
- # property: 'P124',
139
- # datavalue: Wikidatum::DataValueType::MonolingualText.new(
140
- # language: 'en',
141
- # text: 'Foobar'
142
- # )
143
- # )
144
- #
145
- # @example Add a quantity statement.
146
- # wikidatum_client.add_statement(
147
- # id: 'Q123',
148
- # property: 'P124',
149
- # datavalue: Wikidatum::DataValueType::Quantity.new(
150
- # amount: '+12',
151
- # upper_bound: nil,
152
- # lower_bound: nil,
153
- # unit: 'https://wikidata.org/entity/Q1234'
154
- # )
155
- # )
156
- #
157
- # @example Add a time statement.
158
- # wikidatum_client.add_statement(
159
- # id: 'Q123',
160
- # property: 'P124',
161
- # datavalue: Wikidatum::DataValueType::Time.new(
162
- # time: '+2022-08-12T00:00:00Z',
163
- # time_zone: 0,
164
- # precision: 11,
165
- # calendar_model: 'https://wikidata.org/entity/Q1234'
166
- # )
167
- # )
168
- #
169
- # @example Add a Wikibase item statement.
170
- # wikidatum_client.add_statement(
171
- # id: 'Q123',
172
- # property: 'P124',
173
- # datavalue: Wikidatum::DataValueType::WikibaseEntityId.new(
174
- # entity_type: 'item',
175
- # numeric_id: 1234,
176
- # id: 'Q1234'
177
- # )
178
- # )
179
- #
180
- # @param id [String] the ID of the item on which the statement will be added.
181
- # @param property [String] property ID in the format 'P123'.
182
- # @param datavalue [Wikidatum::DataValueType::GlobeCoordinate, Wikidatum::DataValueType::MonolingualText, Wikidatum::DataValueType::Quantity, Wikidatum::DataValueType::WikibaseString, Wikidatum::DataValueType::Time, Wikidatum::DataValueType::WikibaseEntityId, Wikidatum::DataValueType::NoValue, Wikidatum::DataValueType::SomeValue] the datavalue of the statement being created.
183
- # @param datatype [String, nil] if nil, it'll determine the type based on what was passed for the statement argument. This may differ from the type of the Statement's datavalue (for example with the 'url' type).
184
- # @param qualifiers [Hash<String, Array<Wikidatum::Snak>>]
185
- # @param references [Array<Wikidatum::Reference>]
186
- # @param rank [String]
187
- # @param tags [Array<String>]
188
- # @param comment [String, nil]
189
- # @return [Boolean] True if the request succeeded.
190
- def add_statement(id:, property:, datavalue:, datatype: nil, qualifiers: {}, references: [], rank: 'normal', tags: [], comment: nil)
191
- raise ArgumentError, "#{id.inspect} is an invalid Wikibase QID. Must be an integer, a string representation of an integer, or in the format 'Q123'." unless id.is_a?(Integer) || id.match?(ITEM_REGEX)
192
-
193
- id = coerce_item_id(id)
194
-
195
- # Unless datatype is set explicitly by the caller, just assume we can pull the
196
- # default from the datavalue class.
197
- datatype ||= datavalue.wikibase_datatype
198
-
199
- case datavalue.class.to_s
200
- when 'Wikidatum::DataValueType::NoValue'
201
- statement_hash = {
202
- mainsnak: {
203
- snaktype: 'novalue',
204
- property: property,
205
- datatype: datatype
113
+ # Add a statement to an item.
114
+ #
115
+ # NOTE: Adding references/qualifiers with `add_statement` is untested and
116
+ # effectively unsupported for now.
117
+ #
118
+ # @example Add a string statement.
119
+ # wikidatum_client.add_statement(
120
+ # id: 'Q123',
121
+ # property: 'P23',
122
+ # value: Wikidatum::DataType::WikibaseString.new(string: 'Foo'),
123
+ # comment: 'Adding something or another.'
124
+ # )
125
+ #
126
+ # @example Add a 'no value' statement.
127
+ # wikidatum_client.add_statement(
128
+ # id: 'Q123',
129
+ # property: 'P124',
130
+ # value: Wikidatum::DataType::NoValue.new(
131
+ # type: :no_value,
132
+ # value: nil
133
+ # )
134
+ # )
135
+ #
136
+ # @example Add an 'unknown value' statement.
137
+ # wikidatum_client.add_statement(
138
+ # id: 'Q123',
139
+ # property: 'P124',
140
+ # value: Wikidatum::DataType::SomeValue.new(
141
+ # type: :some_value,
142
+ # value: nil
143
+ # )
144
+ # )
145
+ #
146
+ # @example Add a globe coordinate statement.
147
+ # wikidatum_client.add_statement(
148
+ # id: 'Q123',
149
+ # property: 'P124',
150
+ # value: Wikidatum::DataType::GlobeCoordinate.new(
151
+ # latitude: 52.51666,
152
+ # longitude: 13.3833,
153
+ # precision: 0.01666,
154
+ # globe: 'https://wikidata.org/entity/Q2'
155
+ # )
156
+ # )
157
+ #
158
+ # @example Add a monolingual text statement.
159
+ # wikidatum_client.add_statement(
160
+ # id: 'Q123',
161
+ # property: 'P124',
162
+ # value: Wikidatum::DataType::MonolingualText.new(
163
+ # language: 'en',
164
+ # text: 'Foobar'
165
+ # )
166
+ # )
167
+ #
168
+ # @example Add a quantity statement.
169
+ # wikidatum_client.add_statement(
170
+ # id: 'Q123',
171
+ # property: 'P124',
172
+ # value: Wikidatum::DataType::Quantity.new(
173
+ # amount: '+12',
174
+ # unit: 'https://wikidata.org/entity/Q1234'
175
+ # )
176
+ # )
177
+ #
178
+ # @example Add a time statement.
179
+ # wikidatum_client.add_statement(
180
+ # id: 'Q123',
181
+ # property: 'P124',
182
+ # value: Wikidatum::DataType::Time.new(
183
+ # time: '+2022-08-12T00:00:00Z',
184
+ # precision: 11,
185
+ # calendar_model: 'https://wikidata.org/entity/Q1234'
186
+ # )
187
+ # )
188
+ #
189
+ # @example Add a Wikibase item statement.
190
+ # wikidatum_client.add_statement(
191
+ # id: 'Q123',
192
+ # property: 'P124',
193
+ # value: Wikidatum::DataType::WikibaseItem.new(
194
+ # id: 'Q1234'
195
+ # )
196
+ # )
197
+ #
198
+ # @param id [String, Integer] the ID of the item on which the statement will be added.
199
+ # @param property [String, Integer] property ID in the format 'P123', or an integer.
200
+ # @param value [Wikidatum::DataType::GlobeCoordinate, Wikidatum::DataType::MonolingualText, Wikidatum::DataType::Quantity, Wikidatum::DataType::WikibaseString, Wikidatum::DataType::Time, Wikidatum::DataType::WikibaseItem, Wikidatum::DataType::NoValue, Wikidatum::DataType::SomeValue] the value of the statement being created.
201
+ # @param qualifiers [Array<Wikidatum::Qualifier>]
202
+ # @param references [Array<Wikidatum::Reference>]
203
+ # @param rank [String, Symbol] Valid ranks are 'preferred', 'normal', or
204
+ # 'deprecated'. Defaults to 'normal'. Also accepts Symbol for these ranks.
205
+ # @param tags [Array<String>]
206
+ # @param comment [String, nil]
207
+ # @return [Boolean] True if the request succeeded.
208
+ def add_statement(id:, property:, value:, qualifiers: [], references: [], rank: 'normal', tags: [], comment: nil)
209
+ raise ArgumentError, "#{id.inspect} is an invalid Wikibase QID. Must be an integer, a string representation of an integer, or in the format 'Q123'." unless id.is_a?(Integer) || id.match?(ITEM_REGEX)
210
+ raise ArgumentError, "#{property.inspect} is an invalid Wikibase PID. Must be an integer, a string representation of an integer, or in the format 'P123'." unless property.is_a?(Integer) || property.match?(PROPERTY_REGEX)
211
+ raise ArgumentError, "#{rank.inspect} is an invalid rank. Must be normal, preferred, or deprecated." unless VALID_RANKS.include?(rank.to_s)
212
+ raise ArgumentError, "Expected an instance of one of Wikidatum::DataType's subclasses for value, but got #{value.inspect}." unless VALID_DATA_TYPES.include?(value.class.to_s)
213
+
214
+ id = coerce_item_id(id)
215
+ property = coerce_property_id(property)
216
+
217
+ case value.class.to_s
218
+ when 'Wikidatum::DataType::NoValue'
219
+ statement_hash = {
220
+ property: {
221
+ id: property
222
+ },
223
+ value: {
224
+ type: 'novalue'
225
+ }
206
226
  }
207
- }
208
- when 'Wikidatum::DataValueType::SomeValue'
209
- statement_hash = {
210
- mainsnak: {
211
- snaktype: 'somevalue',
212
- property: property,
213
- datatype: datatype
227
+ when 'Wikidatum::DataType::SomeValue'
228
+ statement_hash = {
229
+ property: {
230
+ id: property
231
+ },
232
+ value: {
233
+ type: 'somevalue'
234
+ }
214
235
  }
215
- }
216
- when 'Wikidatum::DataValueType::GlobeCoordinate', 'Wikidatum::DataValueType::MonolingualText', 'Wikidatum::DataValueType::Quantity', 'Wikidatum::DataValueType::WikibaseString', 'Wikidatum::DataValueType::Time', 'Wikidatum::DataValueType::WikibaseEntityId'
217
- statement_hash = {
218
- mainsnak: {
219
- snaktype: 'value',
220
- property: property,
221
- datatype: datatype,
222
- datavalue: {
223
- type: datavalue.wikibase_type,
224
- value: datavalue.marshal_dump
236
+ when 'Wikidatum::DataType::GlobeCoordinate', 'Wikidatum::DataType::MonolingualText', 'Wikidatum::DataType::Quantity', 'Wikidatum::DataType::WikibaseString', 'Wikidatum::DataType::Time', 'Wikidatum::DataType::WikibaseItem'
237
+ statement_hash = {
238
+ property: {
239
+ id: property
240
+ },
241
+ value: {
242
+ type: 'value',
243
+ content: value.marshal_dump
225
244
  }
226
245
  }
227
- }
228
- else
229
- raise ArgumentError, "Expected an instance of one of Wikidatum::DataValueType's subclasses for datavalue, but got #{datavalue.inspect}."
230
- end
246
+ end
231
247
 
232
- body = { statement: statement_hash.merge({ qualifiers: qualifiers, references: references, rank: rank, type: "statement" }) }
248
+ body = { statement: statement_hash.merge({ qualifiers: qualifiers, references: references, rank: rank.to_s, type: 'statement' }) }
233
249
 
234
- response = post_request("/entities/items/#{id}/statements", body, tags: tags, comment: comment)
250
+ response = post_request("/entities/items/#{id}/statements", body, tags: tags, comment: comment)
235
251
 
236
- puts JSON.pretty_generate(response) if ENV['DEBUG']
252
+ puts JSON.pretty_generate(response) if ENV['DEBUG']
237
253
 
238
- response.success?
239
- end
254
+ response.success?
255
+ end
240
256
 
241
- # Delete a statement from an item.
242
- #
243
- # @example
244
- # wikidatum_client.delete_statement(
245
- # id: 'Q123$4543523c-1d1d-1111-1e1e-11b11111b1f1',
246
- # comment: "Deleting this statement because it's bad."
247
- # )
248
- #
249
- # @param id [String] the ID of the statemnt being deleted.
250
- # @param tags [Array<String>]
251
- # @param comment [String, nil]
252
- # @return [Boolean] True if the request succeeded.
253
- def delete_statement(id:, tags: [], comment: nil)
254
- raise ArgumentError, "#{id.inspect} is an invalid Wikibase Statement ID. Must be a string in the format 'Q123$f004ec2b-4857-3b69-b370-e8124f5bd3ac'." unless id.match?(STATEMENT_REGEX)
255
-
256
- response = delete_request("/statements/#{id}", tags: tags, comment: comment)
257
-
258
- puts JSON.pretty_generate(response) if ENV['DEBUG']
259
-
260
- response.success?
261
- end
257
+ # Delete a statement from an item.
258
+ #
259
+ # @example
260
+ # wikidatum_client.delete_statement(
261
+ # id: 'Q123$4543523c-1d1d-1111-1e1e-11b11111b1f1',
262
+ # comment: "Deleting this statement because it's bad."
263
+ # )
264
+ #
265
+ # @param id [String] the ID of the statemnt being deleted.
266
+ # @param tags [Array<String>]
267
+ # @param comment [String, nil]
268
+ # @return [Boolean] True if the request succeeded.
269
+ def delete_statement(id:, tags: [], comment: nil)
270
+ raise ArgumentError, "#{id.inspect} is an invalid Wikibase Statement ID. Must be a string in the format 'Q123$f004ec2b-4857-3b69-b370-e8124f5bd3ac'." unless id.match?(STATEMENT_REGEX)
271
+
272
+ response = delete_request("/statements/#{id}", tags: tags, comment: comment)
273
+
274
+ puts JSON.pretty_generate(response) if ENV['DEBUG']
275
+
276
+ response.success?
277
+ end
262
278
 
263
- private
279
+ # Is the current instance of Client authenticated as a Wikibase user?
280
+ #
281
+ # @return [Boolean]
282
+ def authenticated?
283
+ # TODO: Make it possible for this to be true once authentication
284
+ # is implemented.
285
+ false
286
+ end
264
287
 
265
- # For now this just returns the `@wikibase_url`, but in the future the API
266
- # routes will presumably be nested further, so this is just future-proofing
267
- # to allow that to be easily changed later.
268
- #
269
- # @return [String] URL for the Wikibase API endpoint.
270
- def api_url
271
- @api_url ||= "#{@wikibase_url}/w/rest.php/wikibase/v0"
272
- end
288
+ # Does the current instance of Client allow anonymous IP-based edits?
289
+ #
290
+ # @return [Boolean]
291
+ def allow_ip_edits?
292
+ @allow_ip_edits
293
+ end
273
294
 
274
- # Default headers to be sent with every request.
275
- #
276
- # @return [Hash] A hash of some headers that should be used when sending a request.
277
- def universal_headers
278
- @universal_headers ||= {
279
- 'User-Agent' => @user_agent,
280
- 'Content-Type' => 'application/json'
281
- }
282
- end
295
+ private
283
296
 
284
- # Make a GET request to a given Wikibase endpoint.
285
- #
286
- # @param path [String] The relative path for the API endpoint.
287
- # @param params [Hash] Query parameters to send with the request, if any.
288
- # @return [Hash] JSON response, parsed into a hash.
289
- def get_request(path, params = nil)
290
- url = "#{api_url}#{path}"
291
-
292
- response = Faraday.get(url, params, universal_headers)
293
-
294
- # Error handling if it doesn't return a 200
295
- unless response.success?
296
- puts 'Something went wrong with this request!'
297
- puts "Status Code: #{response.status}"
298
- puts response.body.inspect
297
+ # For now this just returns the `@wikibase_url`, but in the future the API
298
+ # routes will presumably be nested further, so this is just future-proofing
299
+ # to allow that to be easily changed later.
300
+ #
301
+ # @return [String] URL for the Wikibase API endpoint.
302
+ def api_url
303
+ @api_url ||= "#{@wikibase_url}/w/rest.php/wikibase/v0"
299
304
  end
300
305
 
301
- JSON.parse(response.body)
302
- end
303
-
304
- # Make a POST request to a given Wikibase endpoint.
305
- #
306
- # @param path [String] The relative path for the API endpoint.
307
- # @param body [Hash] The body to post to the endpoint.
308
- # @param tags [Array<String>] The tags to apply to the edit being made by this request, for PUT/POST/DELETE requests.
309
- # @param comment [String] The edit description, for PUT/POST/DELETE requests.
310
- # @return [Hash] JSON response, parsed into a hash.
311
- def post_request(path, body = {}, tags: nil, comment: nil)
312
- url = "#{api_url}#{path}"
313
-
314
- body[:bot] = @bot
315
- body[:tags] = tags unless tags.empty?
316
- body[:comment] = comment unless comment.nil?
317
-
318
- response = Faraday.post(url) do |req|
319
- req.body = JSON.generate(body)
320
- req.headers = universal_headers
306
+ # Default headers to be sent with every request.
307
+ #
308
+ # @return [Hash] A hash of some headers that should be used when sending a request.
309
+ def universal_headers
310
+ @universal_headers ||= {
311
+ 'User-Agent' => @user_agent,
312
+ 'Content-Type' => 'application/json'
313
+ }
321
314
  end
322
315
 
323
- puts response.body.inspect if ENV['DEBUG']
316
+ # Make a GET request to a given Wikibase endpoint.
317
+ #
318
+ # @param path [String] The relative path for the API endpoint.
319
+ # @param params [Hash] Query parameters to send with the request, if any.
320
+ # @return [Hash] JSON response, parsed into a hash.
321
+ def get_request(path, params = nil)
322
+ url = "#{api_url}#{path}"
324
323
 
325
- response
326
- end
324
+ response = Faraday.get(url, params, universal_headers)
325
+
326
+ # Error handling if it doesn't return a 200
327
+ unless response.success?
328
+ puts 'Something went wrong with this request!'
329
+ puts "Status Code: #{response.status}"
330
+ puts response.body.inspect
331
+ end
327
332
 
328
- # Make a DELETE request to a given Wikibase endpoint.
329
- #
330
- # @param path [String] The relative path for the API endpoint.
331
- # @param tags [Array<String>] The tags to apply to the edit being made by this request, for PUT/POST/DELETE requests.
332
- # @param comment [String] The edit description, for PUT/POST/DELETE requests.
333
- # @return [Hash] JSON response, parsed into a hash.
334
- def delete_request(path, tags: [], comment: nil)
335
- url = "#{api_url}#{path}"
336
-
337
- body = {}
338
- body[:bot] = @bot
339
- body[:tags] = tags unless tags.empty?
340
- body[:comment] = comment unless comment.nil?
341
-
342
- response = Faraday.delete(url) do |req|
343
- req.body = JSON.generate(body)
344
- req.headers = universal_headers
333
+ JSON.parse(response.body)
345
334
  end
346
335
 
347
- puts response.body.inspect if ENV['DEBUG']
336
+ # Make a POST request to a given Wikibase endpoint.
337
+ #
338
+ # @param path [String] The relative path for the API endpoint.
339
+ # @param body [Hash] The body to post to the endpoint.
340
+ # @param tags [Array<String>] The tags to apply to the edit being made by this request, for PUT/POST/DELETE requests.
341
+ # @param comment [String] The edit description, for PUT/POST/DELETE requests.
342
+ # @return [Hash] JSON response, parsed into a hash.
343
+ def post_request(path, body = {}, tags: [], comment: nil)
344
+ ensure_edit_permitted!
345
+
346
+ url = "#{api_url}#{path}"
347
+
348
+ body[:bot] = @bot
349
+ body[:tags] = tags unless tags.empty?
350
+ body[:comment] = comment unless comment.nil?
351
+
352
+ response = Faraday.post(url) do |req|
353
+ req.body = JSON.generate(body)
354
+ req.headers = universal_headers
355
+ end
356
+
357
+ puts response.body.inspect if ENV['DEBUG']
358
+
359
+ # Error handling if it doesn't return a 200
360
+ unless response.success?
361
+ puts 'Something went wrong with this request!'
362
+ puts "Status Code: #{response.status}"
363
+ puts response.body.inspect
364
+ end
365
+
366
+ response
367
+ end
348
368
 
349
- # Error handling if it doesn't return a 200
350
- unless response.success?
351
- puts 'Something went wrong with this request!'
352
- puts "Status Code: #{response.status}"
353
- puts response.body.inspect
369
+ # Make a DELETE request to a given Wikibase endpoint.
370
+ #
371
+ # @param path [String] The relative path for the API endpoint.
372
+ # @param tags [Array<String>] The tags to apply to the edit being made by this request, for PUT/POST/DELETE requests.
373
+ # @param comment [String] The edit description, for PUT/POST/DELETE requests.
374
+ # @return [Hash] JSON response, parsed into a hash.
375
+ def delete_request(path, tags: [], comment: nil)
376
+ ensure_edit_permitted!
377
+
378
+ url = "#{api_url}#{path}"
379
+
380
+ body = {}
381
+ body[:bot] = @bot
382
+ body[:tags] = tags unless tags.empty?
383
+ body[:comment] = comment unless comment.nil?
384
+
385
+ response = Faraday.delete(url) do |req|
386
+ req.body = JSON.generate(body)
387
+ req.headers = universal_headers
388
+ end
389
+
390
+ puts response.body.inspect if ENV['DEBUG']
391
+
392
+ # Error handling if it doesn't return a 200
393
+ unless response.success?
394
+ puts 'Something went wrong with this request!'
395
+ puts "Status Code: #{response.status}"
396
+ puts response.body.inspect
397
+ end
398
+
399
+ response
354
400
  end
355
401
 
356
- response
357
- end
402
+ # Coerce an Item ID in the formats 'Q123', '123' or 123 into a consistent
403
+ # 'Q123' format. We need to have the ID in the format 'Q123' for the API
404
+ # request, which is why coercion is necessary.
405
+ #
406
+ # @param id [String, Integer]
407
+ # @return [String]
408
+ def coerce_item_id(id)
409
+ return id if id.to_s.start_with?('Q')
410
+
411
+ "Q#{id}"
412
+ end
358
413
 
359
- # Coerce an Item ID in the formats 'Q123', '123' or 123 into a consistent
360
- # 'Q123' format. We need to have the ID in the format 'Q123' for the API
361
- # request, which is why coercion is necessary.
362
- #
363
- # @param id [String, Integer]
364
- # @return [String]
365
- def coerce_item_id(id)
366
- return id if id.to_s.start_with?('Q')
414
+ # Coerce a Property ID in the formats 'P123', '123' or 123 into a consistent
415
+ # 'P123' format. We need to have the ID in the format 'P123' for the API
416
+ # request, which is why coercion is necessary.
417
+ #
418
+ # @param property_id [String, Integer]
419
+ # @return [String]
420
+ def coerce_property_id(property_id)
421
+ return property_id if property_id.to_s.start_with?('P')
367
422
 
368
- "Q#{id}"
423
+ "P#{property_id}"
424
+ end
425
+
426
+ # Check if authentication has been provided, and then check if IP edits
427
+ # are allowed. If neither condition returns true, raise an error.
428
+ #
429
+ # @return [void]
430
+ # @raise [DisallowedIpEditError]
431
+ def ensure_edit_permitted!
432
+ return if authenticated?
433
+ return if allow_ip_edits?
434
+
435
+ raise DisallowedIpEditError
436
+ end
369
437
  end
370
438
  end