aspace_client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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