aspace_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.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/lib/aspace_client.rb +15 -0
  3. data/lib/aspace_client/archivesspace_json_schema.rb +210 -0
  4. data/lib/aspace_client/asutils.rb +142 -0
  5. data/lib/aspace_client/client_enum_source.rb +30 -0
  6. data/lib/aspace_client/exceptions.rb +15 -0
  7. data/lib/aspace_client/helpers.rb +0 -0
  8. data/lib/aspace_client/json_schema_concurrency_fix.rb +52 -0
  9. data/lib/aspace_client/json_schema_utils.rb +414 -0
  10. data/lib/aspace_client/jsonmodel.rb +342 -0
  11. data/lib/aspace_client/jsonmodel_client.rb +528 -0
  12. data/lib/aspace_client/jsonmodel_i18n_mixin.rb +77 -0
  13. data/lib/aspace_client/jsonmodel_type.rb +478 -0
  14. data/lib/aspace_client/memoryleak.rb +59 -0
  15. data/lib/aspace_client/schemas/abstract_agent.rb +51 -0
  16. data/lib/aspace_client/schemas/abstract_agent_relationship.rb +12 -0
  17. data/lib/aspace_client/schemas/abstract_archival_object.rb +96 -0
  18. data/lib/aspace_client/schemas/abstract_classification.rb +44 -0
  19. data/lib/aspace_client/schemas/abstract_name.rb +23 -0
  20. data/lib/aspace_client/schemas/abstract_note.rb +13 -0
  21. data/lib/aspace_client/schemas/accession.rb +174 -0
  22. data/lib/aspace_client/schemas/accession_parts_relationship.rb +31 -0
  23. data/lib/aspace_client/schemas/accession_sibling_relationship.rb +31 -0
  24. data/lib/aspace_client/schemas/active_edits.rb +23 -0
  25. data/lib/aspace_client/schemas/advanced_query.rb +12 -0
  26. data/lib/aspace_client/schemas/agent_contact.rb +25 -0
  27. data/lib/aspace_client/schemas/agent_corporate_entity.rb +32 -0
  28. data/lib/aspace_client/schemas/agent_family.rb +29 -0
  29. data/lib/aspace_client/schemas/agent_person.rb +31 -0
  30. data/lib/aspace_client/schemas/agent_relationship_associative.rb +28 -0
  31. data/lib/aspace_client/schemas/agent_relationship_earlierlater.rb +28 -0
  32. data/lib/aspace_client/schemas/agent_relationship_parentchild.rb +26 -0
  33. data/lib/aspace_client/schemas/agent_relationship_subordinatesuperior.rb +26 -0
  34. data/lib/aspace_client/schemas/agent_software.rb +22 -0
  35. data/lib/aspace_client/schemas/archival_object.rb +60 -0
  36. data/lib/aspace_client/schemas/archival_record_children.rb +15 -0
  37. data/lib/aspace_client/schemas/boolean_field_query.rb +13 -0
  38. data/lib/aspace_client/schemas/boolean_query.rb +13 -0
  39. data/lib/aspace_client/schemas/classification.rb +10 -0
  40. data/lib/aspace_client/schemas/classification_term.rb +38 -0
  41. data/lib/aspace_client/schemas/classification_tree.rb +17 -0
  42. data/lib/aspace_client/schemas/collection_management.rb +27 -0
  43. data/lib/aspace_client/schemas/container.rb +29 -0
  44. data/lib/aspace_client/schemas/container_location.rb +19 -0
  45. data/lib/aspace_client/schemas/date.rb +19 -0
  46. data/lib/aspace_client/schemas/date_field_query.rb +14 -0
  47. data/lib/aspace_client/schemas/deaccession.rb +20 -0
  48. data/lib/aspace_client/schemas/defaults.rb +104 -0
  49. data/lib/aspace_client/schemas/digital_object.rb +64 -0
  50. data/lib/aspace_client/schemas/digital_object_component.rb +53 -0
  51. data/lib/aspace_client/schemas/digital_object_tree.rb +19 -0
  52. data/lib/aspace_client/schemas/digital_record_children.rb +15 -0
  53. data/lib/aspace_client/schemas/enumeration.rb +29 -0
  54. data/lib/aspace_client/schemas/enumeration_migration.rb +14 -0
  55. data/lib/aspace_client/schemas/event.rb +88 -0
  56. data/lib/aspace_client/schemas/extent.rb +17 -0
  57. data/lib/aspace_client/schemas/external_document.rb +12 -0
  58. data/lib/aspace_client/schemas/external_id.rb +11 -0
  59. data/lib/aspace_client/schemas/field_query.rb +15 -0
  60. data/lib/aspace_client/schemas/file_version.rb +26 -0
  61. data/lib/aspace_client/schemas/group.rb +17 -0
  62. data/lib/aspace_client/schemas/instance.rb +27 -0
  63. data/lib/aspace_client/schemas/job.rb +57 -0
  64. data/lib/aspace_client/schemas/location.rb +36 -0
  65. data/lib/aspace_client/schemas/location_batch.rb +45 -0
  66. data/lib/aspace_client/schemas/merge_request.rb +48 -0
  67. data/lib/aspace_client/schemas/name_corporate_entity.rb +15 -0
  68. data/lib/aspace_client/schemas/name_family.rb +13 -0
  69. data/lib/aspace_client/schemas/name_form.rb +15 -0
  70. data/lib/aspace_client/schemas/name_person.rb +19 -0
  71. data/lib/aspace_client/schemas/name_software.rb +14 -0
  72. data/lib/aspace_client/schemas/note_abstract.rb +17 -0
  73. data/lib/aspace_client/schemas/note_bibliography.rb +29 -0
  74. data/lib/aspace_client/schemas/note_bioghist.rb +22 -0
  75. data/lib/aspace_client/schemas/note_chronology.rb +28 -0
  76. data/lib/aspace_client/schemas/note_citation.rb +32 -0
  77. data/lib/aspace_client/schemas/note_definedlist.rb +25 -0
  78. data/lib/aspace_client/schemas/note_digital_object.rb +23 -0
  79. data/lib/aspace_client/schemas/note_index.rb +29 -0
  80. data/lib/aspace_client/schemas/note_index_item.rb +25 -0
  81. data/lib/aspace_client/schemas/note_multipart.rb +25 -0
  82. data/lib/aspace_client/schemas/note_orderedlist.rb +27 -0
  83. data/lib/aspace_client/schemas/note_outline.rb +20 -0
  84. data/lib/aspace_client/schemas/note_outline_level.rb +21 -0
  85. data/lib/aspace_client/schemas/note_singlepart.rb +24 -0
  86. data/lib/aspace_client/schemas/note_text.rb +17 -0
  87. data/lib/aspace_client/schemas/permission.rb +15 -0
  88. data/lib/aspace_client/schemas/preference.rb +16 -0
  89. data/lib/aspace_client/schemas/record_tree.rb +17 -0
  90. data/lib/aspace_client/schemas/repository.rb +32 -0
  91. data/lib/aspace_client/schemas/repository_with_agent.rb +14 -0
  92. data/lib/aspace_client/schemas/resource.rb +112 -0
  93. data/lib/aspace_client/schemas/resource_tree.rb +20 -0
  94. data/lib/aspace_client/schemas/rights_statement.rb +35 -0
  95. data/lib/aspace_client/schemas/subject.rb +30 -0
  96. data/lib/aspace_client/schemas/term.rb +16 -0
  97. data/lib/aspace_client/schemas/user.rb +56 -0
  98. data/lib/aspace_client/schemas/user_defined.rb +42 -0
  99. data/lib/aspace_client/schemas/vocabulary.rb +15 -0
  100. data/lib/aspace_client/validations.rb +434 -0
  101. data/lib/aspace_client/validator_cache.rb +47 -0
  102. data/lib/aspace_client/version.rb +3 -0
  103. metadata +244 -0
@@ -0,0 +1,528 @@
1
+ require 'net/http/persistent'
2
+ require 'net/http/post/multipart'
3
+ require 'json'
4
+ require_relative 'exceptions'
5
+
6
+
7
+ module JSONModel
8
+
9
+ # Set the repository that subsequent operations will apply to.
10
+ def self.set_repository(id)
11
+ Thread.current[:selected_repo_id] = id
12
+ end
13
+
14
+
15
+ # The currently selected repository (if any)
16
+ def self.repository
17
+ Thread.current[:selected_repo_id]
18
+ end
19
+
20
+
21
+ # Grab an array of JSON objects from 'uri' and use the 'type_descriptor'
22
+ # property of each object to cast it into a JSONModel.
23
+ def self.all(uri, type_descriptor)
24
+ JSONModel::HTTP.get_json(uri).map do |obj|
25
+ JSONModel(obj[type_descriptor.to_s]).new(obj)
26
+ end
27
+ end
28
+
29
+
30
+ def self.with_repository(id)
31
+ old_repo = Thread.current[:selected_repo_id]
32
+ begin
33
+ self.set_repository(id)
34
+ yield
35
+ ensure
36
+ self.set_repository(old_repo)
37
+ end
38
+ end
39
+
40
+
41
+ def self.backend_url
42
+ if Module.const_defined?(:BACKEND_SERVICE_URL)
43
+ BACKEND_SERVICE_URL
44
+ else
45
+ init_args[:url]
46
+ end
47
+ end
48
+
49
+
50
+ @@error_handlers = []
51
+
52
+ def self.add_error_handler(&block)
53
+ @@error_handlers << block
54
+ end
55
+
56
+ def self.handle_error(err)
57
+ @@error_handlers.each do |handler|
58
+ handler.call(err)
59
+ end
60
+ end
61
+
62
+
63
+ module Notification
64
+ @@notification_handlers = []
65
+
66
+ def self.add_notification_handler(code = nil, &block)
67
+ @@notification_handlers << {:code => code, :block => block}
68
+ end
69
+
70
+ def self.start_background_thread
71
+ Thread.new do
72
+ sequence = 0
73
+
74
+ while true
75
+ begin
76
+ notifications = JSONModel::HTTP::get_json('/notifications',
77
+ :last_sequence => sequence)
78
+
79
+ notifications.each do |notification|
80
+ @@notification_handlers.each do |handler|
81
+ if handler[:code].nil? or handler[:code] == notification["code"]
82
+ begin
83
+ handler[:block].call(notification["code"], notification["params"])
84
+ rescue
85
+ $stderr.puts("ERROR: Failed to handle notification #{notification.inspect}: #{$!}")
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ sequence = notifications.last['sequence']
92
+ rescue
93
+ sleep 5
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ end
100
+
101
+
102
+
103
+ module HTTP
104
+
105
+ def self.backend_url
106
+ if Module.const_defined?(:BACKEND_SERVICE_URL)
107
+ BACKEND_SERVICE_URL
108
+ else
109
+ JSONModel::init_args[:url]
110
+ end
111
+ end
112
+
113
+
114
+ # We override this in the backend's spec_helper since Rack::Test::Methods
115
+ # doesn't support multipart requests.
116
+ def self.multipart_request(uri, params)
117
+ Net::HTTP::Post::Multipart.new(uri, params)
118
+ end
119
+
120
+
121
+ def self.form_urlencoded(uri, params)
122
+ request = Net::HTTP::Post.new(uri)
123
+ request.form_data = params
124
+ request
125
+ end
126
+
127
+
128
+ # Perform a HTTP POST request against the backend with form parameters
129
+ #
130
+ # `encoding' is either :x_www_form_urlencoded or :multipart_form_data. The
131
+ # latter is useful if you're providing a file upload.
132
+ def self.post_form(uri, params = {}, encoding = :x_www_form_urlencoded)
133
+ url = URI("#{backend_url}#{uri}")
134
+
135
+ req = if encoding == :x_www_form_urlencoded
136
+ self.form_urlencoded(url.request_uri, params)
137
+ elsif encoding == :multipart_form_data
138
+ self.multipart_request(url.request_uri, params)
139
+ else
140
+ raise "Unknown form encoding: #{encoding.inspect}"
141
+ end
142
+
143
+ do_http_request(url, req)
144
+ end
145
+
146
+
147
+ def self.stream(uri, params = {}, &block)
148
+ uri = URI("#{backend_url}#{uri}")
149
+ uri.query = URI.encode_www_form(params)
150
+
151
+ req = Net::HTTP::Get.new(uri.request_uri)
152
+
153
+ req['X-ArchivesSpace-Session'] = current_backend_session
154
+
155
+ if high_priority?
156
+ req['X-ArchivesSpace-Priority'] = "high"
157
+ end
158
+
159
+ Net::HTTP.start(uri.host, uri.port) do |http|
160
+ http.request(req, nil) do |response|
161
+ if response.code =~ /^4/
162
+ JSONModel::handle_error(ASUtils.json_parse(response.body))
163
+ raise response.body
164
+ end
165
+
166
+ block.call(response)
167
+ end
168
+ end
169
+ end
170
+
171
+
172
+ def self.get_json(uri, params = {})
173
+ uri = URI("#{backend_url}#{uri}")
174
+ uri.query = URI.encode_www_form(params)
175
+
176
+ response = get_response(uri)
177
+
178
+ if response.is_a?(Net::HTTPSuccess) || response.status == 200
179
+ ASUtils.json_parse(response.body)
180
+ else
181
+ nil
182
+ end
183
+ end
184
+
185
+
186
+ # Returns the session token to be sent to the backend when making
187
+ # requests.
188
+ def self.current_backend_session
189
+ # Set by the ApplicationController
190
+ Thread.current[:backend_session]
191
+ end
192
+
193
+
194
+ def self.current_backend_session=(val)
195
+ Thread.current[:backend_session] = val
196
+ end
197
+
198
+
199
+ def self.high_priority?
200
+ if Thread.current[:request_priority]
201
+ Thread.current[:request_priority] == :high
202
+ else
203
+ JSONModel::init_args[:priority] == :high
204
+ end
205
+ end
206
+
207
+
208
+ def self.http_conn
209
+ @http ||= Net::HTTP::Persistent.new 'jsonmodel_client'
210
+ @http.read_timeout = 1200
211
+ @http
212
+ end
213
+
214
+
215
+ def self.do_http_request(url, req, &block)
216
+ req['X-ArchivesSpace-Session'] = current_backend_session
217
+
218
+ if high_priority?
219
+ req['X-ArchivesSpace-Priority'] = "high"
220
+ end
221
+
222
+ response = http_conn.request(url, req, &block)
223
+
224
+ if response.code =~ /^4/
225
+ JSONModel::handle_error(ASUtils.json_parse(response.body))
226
+ end
227
+
228
+ response
229
+ end
230
+
231
+
232
+ def self.with_request_priority(priority)
233
+ old = Thread.current[:request_priority]
234
+ Thread.current[:request_priority] = priority
235
+ begin
236
+ yield
237
+ ensure
238
+ Thread.current[:request_priority] = old
239
+ end
240
+ end
241
+
242
+
243
+ def self.post_json(url, json)
244
+ req = Net::HTTP::Post.new(url.request_uri)
245
+ req['Content-Type'] = 'text/json'
246
+ req.body = json
247
+
248
+ do_http_request(url, req)
249
+ end
250
+
251
+
252
+ def self.post_json_file(url, path, &block)
253
+ File.open(path) do |fh|
254
+ req = Net::HTTP::Post.new(url.request_uri)
255
+ req['Content-Type'] = 'text/json'
256
+ req['Content-Length'] = File.size(path)
257
+ req.body_stream = fh
258
+
259
+ do_http_request(url, req, &block)
260
+ end
261
+ end
262
+
263
+
264
+ def self.delete_request(url)
265
+ req = Net::HTTP::Delete.new(url.request_uri)
266
+
267
+ do_http_request(url, req)
268
+ end
269
+
270
+
271
+ def self.get_response(url)
272
+ req = Net::HTTP::Get.new(url.request_uri)
273
+
274
+ do_http_request(url, req)
275
+ end
276
+
277
+ end
278
+
279
+
280
+
281
+ module Client
282
+
283
+ def self.included(base)
284
+ base.extend(ClassMethods)
285
+ end
286
+
287
+
288
+ # Validate this JSONModel instance, produce a JSON string, and send an
289
+ # update to the backend.
290
+ def save(opts = {}, whole_body = false)
291
+
292
+ clear_errors
293
+
294
+ type = self.class.record_type
295
+ response = JSONModel::HTTP.post_json(self.class.my_url(self.id, opts),
296
+ self.to_json)
297
+
298
+ if response.code == '200'
299
+ response = ASUtils.json_parse(response.body)
300
+
301
+ self.uri = self.class.uri_for(response["id"], opts)
302
+
303
+ # If we were able to save successfully, increment our local version
304
+ # number to match the version on the server.
305
+ self.lock_version = response["lock_version"]
306
+
307
+ # Ensure object is up to date
308
+ if response["stale"]
309
+ self.refetch
310
+ end
311
+
312
+ return whole_body ? response : response["id"]
313
+
314
+ elsif response.code == '403'
315
+ raise AccessDeniedException.new
316
+
317
+ elsif response.code == '409'
318
+ err = ASUtils.json_parse(response.body)
319
+ raise ConflictException.new(err["error"])
320
+
321
+ elsif response.code == '404'
322
+ raise RecordNotFound.new
323
+
324
+ elsif response.code =~ /^4/
325
+ err = ASUtils.json_parse(response.body)
326
+
327
+ if err["error"].is_a?(Hash)
328
+ err["error"].each do |field, errors|
329
+ errors.each do |msg|
330
+ add_error(field, msg)
331
+ end
332
+ end
333
+ end
334
+
335
+ raise ValidationException.new(:invalid_object => self,
336
+ :errors => err["error"])
337
+ else
338
+ raise Exception.new("Unknown response: #{response.body} (code: #{response.code})")
339
+ end
340
+ end
341
+
342
+
343
+ def refetch
344
+ # if a new object, nothing to fetch
345
+ return self if self.id.nil?
346
+
347
+ obj = (self.instance_data.has_key? :find_opts) ?
348
+ self.class.find(self.id, self.instance_data[:find_opts]) : self.class.find(self.id)
349
+
350
+ self.reset_from(obj) if not obj.nil?
351
+ end
352
+
353
+
354
+ def delete
355
+ response = JSONModel::HTTP.delete_request(self.class.my_url(self.id))
356
+
357
+ if response.code == '200'
358
+ true
359
+ elsif response.code == '403'
360
+ raise AccessDeniedException.new
361
+ elsif response.code == '404'
362
+ nil
363
+ elsif response.code == '409'
364
+ err = ASUtils.json_parse(response.body)
365
+ raise ConflictException.new(err["error"])
366
+ else
367
+ raise Exception.new("Unknown response: #{response}")
368
+ end
369
+ end
370
+
371
+ # Mark the suppression status of this record
372
+ def set_suppressed(val)
373
+ response = JSONModel::HTTP.post_form("#{self.uri}/suppressed", :suppressed => val)
374
+
375
+ if response.code == '403'
376
+ raise AccessDeniedException.new("Permission denied when setting suppression status")
377
+ elsif response.code != '200'
378
+ raise "Error when setting suppression status for #{self}: #{response.code} -- #{response.body}"
379
+ end
380
+
381
+ self["suppressed"] = ASUtils.json_parse(response.body)["suppressed_state"]
382
+ end
383
+
384
+
385
+ def suppress
386
+ set_suppressed(true)
387
+ end
388
+
389
+
390
+ def unsuppress
391
+ set_suppressed(false)
392
+ end
393
+
394
+
395
+ def add_error(field, message)
396
+ @errors ||= {}
397
+ @errors[field.to_s] ||= []
398
+ @errors[field.to_s] << message
399
+ end
400
+
401
+
402
+ module ClassMethods
403
+
404
+ def self.extended(base)
405
+ class << base
406
+ alias :_substitute_parameters :substitute_parameters
407
+
408
+ def substitute_parameters(uri, opts = {})
409
+ opts = ASUtils.keys_as_strings(opts)
410
+ if JSONModel::repository
411
+ opts = {'repo_id' => JSONModel::repository}.merge(opts)
412
+ end
413
+
414
+ _substitute_parameters(uri, opts)
415
+ end
416
+ end
417
+ end
418
+
419
+
420
+ # Given the ID of a JSONModel instance, return its full URL (including the
421
+ # URL of the backend)
422
+ def my_url(id = nil, opts = {})
423
+ uri, remaining_opts = self.uri_and_remaining_options_for(id, opts)
424
+
425
+ url = URI("#{JSONModel::HTTP.backend_url}#{uri}")
426
+
427
+ # Don't need to pass this as a URL parameter if it wasn't picked up by
428
+ # the URI template substitution.
429
+ remaining_opts.delete(:repo_id)
430
+
431
+ if not remaining_opts.empty?
432
+ url.query = URI.encode_www_form(remaining_opts)
433
+ end
434
+
435
+ url
436
+ end
437
+
438
+
439
+ # Given an ID, retrieve an instance of the current JSONModel from the
440
+ # backend.
441
+ def find(id, opts = {})
442
+ response = JSONModel::HTTP.get_response(my_url(id, opts))
443
+
444
+ if response.code == '200'
445
+ obj = self.new(ASUtils.json_parse(response.body))
446
+ # store find params on instance to support #refetch
447
+ obj.instance_data[:find_opts] = opts
448
+ obj
449
+ elsif response.code == '403'
450
+ raise AccessDeniedException.new
451
+ elsif response.code == '404'
452
+ raise RecordNotFound.new
453
+ else
454
+ raise response.body
455
+ end
456
+ end
457
+
458
+
459
+ def find_by_uri(uri, opts = {})
460
+ self.find(self.id_for(uri), opts)
461
+ end
462
+
463
+
464
+ # Return all instances of the current JSONModel's record type.
465
+ def all(params = {}, opts = {})
466
+ uri = my_url(nil, opts)
467
+
468
+ uri.query = URI.encode_www_form(params)
469
+
470
+ response = JSONModel::HTTP.get_response(uri)
471
+
472
+ if response.code == '200'
473
+ json_list = ASUtils.json_parse(response.body)
474
+
475
+ if json_list.is_a?(Hash)
476
+ json_list["results"] = json_list["results"].map {|h| self.new(h)}
477
+ else
478
+ json_list = json_list.map {|h| self.new(h)}
479
+ end
480
+
481
+ json_list
482
+ elsif response.code == '403'
483
+ raise AccessDeniedException.new
484
+ else
485
+ raise response.body
486
+ end
487
+ end
488
+
489
+ end
490
+
491
+
492
+ class EnumSource
493
+
494
+ def self.fetch_enumerations
495
+ enumerations = {}
496
+ enumerations[:defaults] = {}
497
+ JSONModel::JSONModel(:enumeration).all.each do |enumeration|
498
+ enumerations[enumeration.name] = enumeration.values
499
+ enumerations[:defaults][enumeration.name] = enumeration.default_value
500
+ end
501
+
502
+ enumerations
503
+ end
504
+
505
+
506
+ def initialize
507
+ @enumerations = self.class.fetch_enumerations
508
+ end
509
+
510
+
511
+ def valid?(name, value)
512
+ values_for(name).include?(value)
513
+ end
514
+
515
+
516
+ def values_for(name)
517
+ @enumerations.fetch(name)
518
+ end
519
+
520
+ def default_value_for
521
+ @enumerations[:defaults].fetch(name)
522
+ end
523
+
524
+ end
525
+
526
+
527
+ end
528
+ end