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,434 @@
1
+ require 'date'
2
+ require 'time'
3
+
4
+ module JSONModel::Validations
5
+ extend JSONModel
6
+
7
+
8
+ def self.check_identifier(hash)
9
+ ids = (0...4).map {|i| hash["id_#{i}"]}
10
+
11
+ errors = []
12
+
13
+ if ids.reverse.drop_while {|elt| elt.to_s.empty?}.any?{|elt| elt.to_s.empty?}
14
+ errors << ["identifier", "must not contain blank entries"]
15
+ end
16
+
17
+ errors
18
+ end
19
+
20
+
21
+ [:archival_object, :accession, :resource].each do |type|
22
+ if JSONModel(type)
23
+ JSONModel(type).add_validation("#{type}_check_identifier") do |hash|
24
+ check_identifier(hash)
25
+ end
26
+ end
27
+ end
28
+
29
+ # Specification:
30
+ # https://www.pivotaltracker.com/story/show/41430143
31
+ # See also: https://www.pivotaltracker.com/story/show/51373893
32
+ def self.check_source(hash)
33
+ errors = []
34
+
35
+ # non-authorized forms don't need source or rules
36
+ return errors if !hash['authorized']
37
+
38
+ if hash["source"].nil?
39
+ if hash["rules"].nil?
40
+ errors << ["rules", "is required when 'source' is blank"]
41
+ errors << ["source", "is required when 'rules' is blank"]
42
+ end
43
+ end
44
+
45
+ errors
46
+ end
47
+
48
+ # https://www.pivotaltracker.com/story/show/51373893
49
+ def self.check_authority_id(hash)
50
+ warnings = []
51
+ if hash["source"].nil? && hash["authority_id"]
52
+ warnings << ["source", "is required if there is an authority id"]
53
+ end
54
+
55
+ warnings
56
+ end
57
+
58
+ def self.check_name(hash)
59
+ errors = []
60
+ errors << ["sort_name", "Property is required but was missing"] if hash["sort_name"].nil? and !hash["sort_name_auto_generate"]
61
+ errors
62
+ end
63
+
64
+ [:name_person, :name_family, :name_corporate_entity, :name_software].each do |type|
65
+ if JSONModel(type)
66
+ JSONModel(type).add_validation("#{type}_check_source") do |hash|
67
+ check_source(hash)
68
+ end
69
+ JSONModel(type).add_validation("#{type}_check_name") do |hash|
70
+ check_name(hash)
71
+ end
72
+ JSONModel(type).add_validation("#{type}_check_authority_id", :warning) do |hash|
73
+ check_authority_id(hash)
74
+ end
75
+ end
76
+ end
77
+
78
+
79
+ # Take a date like YYYY or YYYY-MM and pad to YYYY-MM-DD
80
+ #
81
+ # Note: this might not yield a valid date. The only goal is that something
82
+ # valid on the way in remains valid on the way out.
83
+ #
84
+ def self.normalise_date(date)
85
+ negated = date.start_with?("-")
86
+
87
+ parts = date.gsub(/^-/, '').split(/-/)
88
+
89
+ # Pad out to the right length
90
+ padded = (parts + ['01', '01']).take(3)
91
+
92
+ (negated ? "-" : "") + padded.join("-")
93
+ end
94
+
95
+
96
+ # Returns a valid date or throws if the input is invalid.
97
+ def self.parse_sloppy_date(s)
98
+ begin
99
+ Date.strptime(normalise_date(s), '%Y-%m-%d')
100
+ rescue
101
+ raise ArgumentError.new($!)
102
+ end
103
+ end
104
+
105
+
106
+ def self.check_date(hash)
107
+ errors = []
108
+
109
+ begin
110
+ begin_date = parse_sloppy_date(hash['begin']) if hash['begin']
111
+ rescue ArgumentError => e
112
+ errors << ["begin", "not a valid date"]
113
+ end
114
+
115
+ begin
116
+ if hash['end']
117
+ # If padding our end date with months/days would cause it to fall before
118
+ # the start date (e.g. if the start date was '2000-05' and the end date
119
+ # just '2000'), use the start date in place of end.
120
+ end_s = if begin_date && hash['begin'] && hash['begin'].start_with?(hash['end'])
121
+ hash['begin']
122
+ else
123
+ hash['end']
124
+ end
125
+
126
+ end_date = parse_sloppy_date(end_s)
127
+ end
128
+ rescue ArgumentError
129
+ errors << ["end", "not a valid date"]
130
+ end
131
+
132
+ if begin_date && end_date && end_date < begin_date
133
+ errors << ["end", "must not be before begin"]
134
+ end
135
+
136
+ if hash["expression"].nil? && hash["begin"].nil? && hash["end"].nil?
137
+ errors << ["expression", "is required unless a begin or end date is given"]
138
+ errors << ["begin", "is required unless an expression or an end date is given"]
139
+ errors << ["end", "is required unless an expression or a begin date is given"]
140
+ end
141
+
142
+ errors
143
+ end
144
+
145
+
146
+ if JSONModel(:date)
147
+ JSONModel(:date).add_validation("check_date") do |hash|
148
+ check_date(hash)
149
+ end
150
+ end
151
+
152
+
153
+ def self.check_rights_statement(hash)
154
+ errors = []
155
+
156
+ if hash["rights_type"] == "intellectual_property"
157
+ errors << ["ip_status", "missing required property"] if hash["ip_status"].nil?
158
+ errors << ["jurisdiction", "missing required property"] if hash["jurisdiction"].nil?
159
+ elsif hash["rights_type"] == "license"
160
+ errors << ["license_identifier_terms", "missing required property"] if hash["license_identifier_terms"].nil?
161
+ elsif hash["rights_type"] == "statute"
162
+ errors << ["statute_citation", "missing required property"] if hash["statute_citation"].nil?
163
+ errors << ["jurisdiction", "missing required property"] if hash["jurisdiction"].nil?
164
+ end
165
+
166
+ errors
167
+ end
168
+
169
+
170
+ if JSONModel(:rights_statement)
171
+ JSONModel(:rights_statement).add_validation("check_rights_statement") do |hash|
172
+ check_rights_statement(hash)
173
+ end
174
+ end
175
+
176
+
177
+ def self.check_location(hash)
178
+ errors = []
179
+
180
+ # When creating a location, a minimum of one of the following is required:
181
+ # * Barcode
182
+ # * Classification
183
+ # * Coordinate 1 Label AND Coordinate 1 Indicator
184
+ required_location_fields = [["barcode"],
185
+ ["classification"],
186
+ ["coordinate_1_indicator", "coordinate_1_label"]]
187
+
188
+ if !required_location_fields.any? { |fieldset| fieldset.all? {|field| hash[field]} }
189
+ errors << :location_fields_error
190
+ end
191
+
192
+ errors
193
+ end
194
+
195
+
196
+ if JSONModel(:location)
197
+ JSONModel(:location).add_validation("check_location") do |hash|
198
+ check_location(hash)
199
+ end
200
+ end
201
+
202
+
203
+ def self.check_container_location(hash)
204
+ errors = []
205
+
206
+ errors << ["end_date", "is required"] if hash["end_date"].nil? and hash["status"] == "previous"
207
+
208
+ errors
209
+ end
210
+
211
+
212
+ if JSONModel(:container_location)
213
+ JSONModel(:container_location).add_validation("check_container_location") do |hash|
214
+ check_container_location(hash)
215
+ end
216
+ end
217
+
218
+
219
+ def self.check_container(hash)
220
+ errors = []
221
+ got_current = false
222
+
223
+ required_container_fields = [["barcode_1"],
224
+ ["type_1", "indicator_1"]]
225
+
226
+ if !required_container_fields.any? { |fieldset| fieldset.all? {|field| hash[field]} }
227
+ errors << [ :type_1, "either type_1 or barcode is required" ]
228
+ end
229
+
230
+ if !hash["container_extent_number"].nil? and hash["container_extent_number"] !~ /^\-?\d{0,9}(\.\d{1,5})?$/
231
+ errors << ["container_extent", "must be a number with no more than nine digits and five decimal places"]
232
+ elsif !hash["container_extent"].nil? and hash["container_extent_type"].nil?
233
+ errors << ["container_extent_type", "is required if container extent is specified "]
234
+ end
235
+
236
+ Array(hash["container_locations"]).each do |loc|
237
+ if loc["status"] == "current"
238
+ if got_current
239
+ errors << ["container_locations", "only one location can be current"]
240
+ break
241
+ else
242
+ got_current = true
243
+ end
244
+ end
245
+ end
246
+
247
+ errors
248
+ end
249
+
250
+
251
+ if JSONModel(:container)
252
+ JSONModel(:container).add_validation("check_container") do |hash|
253
+ check_container(hash)
254
+ end
255
+ end
256
+
257
+
258
+ def self.check_instance(hash)
259
+ errors = []
260
+
261
+ if hash["instance_type"] == "digital_object"
262
+ errors << ["digital_object", "Can't be empty"] if hash["digital_object"].nil?
263
+
264
+ elsif hash["instance_type"]
265
+ errors << ["container", "Can't be empty"] if hash["container"].nil?
266
+ end
267
+
268
+ errors
269
+ end
270
+
271
+
272
+ if JSONModel(:instance)
273
+ JSONModel(:instance).add_validation("check_instance") do |hash|
274
+ check_instance(hash)
275
+ end
276
+ end
277
+
278
+
279
+ def self.check_collection_management(hash)
280
+ errors = []
281
+
282
+ if !hash["processing_total_extent"].nil? and hash["processing_total_extent_type"].nil?
283
+ errors << ["processing_total_extent_type", "is required if total extent is specified"]
284
+ end
285
+
286
+ errors
287
+ end
288
+
289
+
290
+ if JSONModel(:collection_management)
291
+ JSONModel(:collection_management).add_validation("check_collection_management") do |hash|
292
+ check_collection_management(hash)
293
+ end
294
+ end
295
+
296
+
297
+ def self.check_user_defined(hash)
298
+ errors = []
299
+
300
+ ["integer_1", "integer_2", "integer_3"].each do |k|
301
+ if !hash[k].nil? and hash[k] !~ /^\-?\d+$/
302
+ errors << [k, "must be an integer"]
303
+ end
304
+ end
305
+
306
+ ["real_1", "real_2", "real_3"].each do |k|
307
+ if !hash[k].nil? and hash[k] !~ /^\-?\d{0,9}(\.\d{1,5})?$/
308
+ errors << [k, "must be a number with no more than nine digits and five decimal places"]
309
+ end
310
+ end
311
+
312
+ errors
313
+ end
314
+
315
+
316
+ if JSONModel(:user_defined)
317
+ JSONModel(:user_defined).add_validation("check_user-defined") do |hash|
318
+ check_user_defined(hash)
319
+ end
320
+ end
321
+
322
+
323
+ if JSONModel(:resource)
324
+ JSONModel(:resource).add_validation("check_resource_otherlevel", :warning) do |hash|
325
+ check_otherlevel(hash)
326
+ end
327
+ end
328
+
329
+
330
+ def self.check_otherlevel(hash)
331
+ warnings = []
332
+
333
+ if hash["level"] == "otherlevel"
334
+ warnings << ["other_level", "is required"] if hash["other_level"].nil?
335
+ end
336
+
337
+ warnings
338
+ end
339
+
340
+ def self.check_archival_object(hash)
341
+ errors = []
342
+
343
+ if (!hash.has_key?("dates") || hash["dates"].empty?) && (!hash.has_key?("title") || hash["title"].empty?)
344
+ errors << ["dates", "one or more required (or enter a Title)"]
345
+ errors << ["title", "must not be an empty string (or enter a Date)"]
346
+ end
347
+
348
+ errors
349
+ end
350
+
351
+
352
+ if JSONModel(:archival_object)
353
+ JSONModel(:archival_object).add_validation("check_archival_object") do |hash|
354
+ check_archival_object(hash)
355
+ end
356
+
357
+ JSONModel(:archival_object).add_validation("check_archival_object_otherlevel", :warning) do |hash|
358
+ check_otherlevel(hash);
359
+ end
360
+
361
+ end
362
+
363
+
364
+ def self.check_digital_object_component(hash)
365
+ errors = []
366
+
367
+ fields = ["dates", "title", "label"]
368
+
369
+ if fields.all? {|field| !hash.has_key?(field) || hash[field].empty?}
370
+ fields.each do |field|
371
+ errors << [field, "you must provide a label, title or date"]
372
+ end
373
+ end
374
+
375
+ errors
376
+ end
377
+
378
+
379
+ JSONModel(:digital_object_component).add_validation("check_digital_object_component") do |hash|
380
+ check_digital_object_component(hash)
381
+ end
382
+
383
+
384
+ JSONModel(:event).add_validation("check_event") do |hash|
385
+ errors = []
386
+
387
+ if hash.has_key?("date") && hash.has_key?("timestamp")
388
+ errors << ["date", "Can't specify both a date and a timestamp"]
389
+ errors << ["timestamp", "Can't specify both a date and a timestamp"]
390
+ end
391
+
392
+ if !hash.has_key?("date") && !hash.has_key?("timestamp")
393
+ errors << ["date", "Must specify either a date or a timestamp"]
394
+ errors << ["timestamp", "Must specify either a date or a timestamp"]
395
+ end
396
+
397
+ if hash["timestamp"]
398
+ # Make sure we can parse it
399
+ begin
400
+ Time.parse(hash["timestamp"])
401
+ rescue ArgumentError
402
+ errors << ["timestamp", "Must be an ISO8601-formatted string"]
403
+ end
404
+ end
405
+
406
+ errors
407
+ end
408
+
409
+
410
+ [:agent_person, :agent_family, :agent_software, :agent_corporate_entity].each do |agent_type|
411
+
412
+ JSONModel(agent_type).add_validation("check_#{agent_type.to_s}") do |hash|
413
+ errors = []
414
+
415
+ if hash.has_key?("dates_of_existence") && hash["dates_of_existence"].find {|d| d['label'] != 'existence' }
416
+ errors << ["dates_of_existence", "Label must be 'existence' in this context"]
417
+ end
418
+
419
+ errors
420
+ end
421
+
422
+ end
423
+
424
+ [:note_multipart, :note_bioghist].each do |schema|
425
+ JSONModel(schema).add_validation("#{schema}_check_at_least_one_subnote") do |hash|
426
+ if Array(hash['subnotes']).empty?
427
+ [["subnotes", "Must contain at least one subnote"]]
428
+ else
429
+ []
430
+ end
431
+ end
432
+ end
433
+
434
+ end
@@ -0,0 +1,47 @@
1
+ class ValidatorCache
2
+
3
+ def self.create_validator(jsonmodel, data)
4
+ JSON::Validator.new(jsonmodel.schema,
5
+ data,
6
+ :errors_as_objects => true,
7
+ :record_errors => true)
8
+ end
9
+
10
+ def self.with_validator_for(jsonmodel, data)
11
+ Thread.current[:validator_cache] ||= {}
12
+
13
+ created = false
14
+ if Thread.current[:validator_cache][jsonmodel]
15
+
16
+ # If we have a cache entry but it's in use, just return a newly allocated
17
+ # validator.
18
+ if Thread.current[:validator_cache][jsonmodel][:in_use]
19
+ return self.create_validator(jsonmodel, data)
20
+ end
21
+
22
+ else
23
+ # If there's no entry, add one
24
+ Thread.current[:validator_cache][jsonmodel] = {}
25
+ Thread.current[:validator_cache][jsonmodel][:validator] = self.create_validator(jsonmodel, data)
26
+ created = true
27
+ end
28
+
29
+ validator = Thread.current[:validator_cache][jsonmodel][:validator]
30
+
31
+ # Reuse this existing validator by setting its data
32
+ if !created
33
+ validator.instance_eval do
34
+ @data = data
35
+ end
36
+ end
37
+
38
+ Thread.current[:validator_cache][jsonmodel][:in_use] = true
39
+
40
+ begin
41
+ yield(validator)
42
+ ensure
43
+ Thread.current[:validator_cache][jsonmodel][:in_use] = false
44
+ end
45
+ end
46
+
47
+ end