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,414 @@
1
+ module JSONSchemaUtils
2
+
3
+ def self.fragment_join(fragment, property = nil)
4
+ fragment = fragment.gsub(/^#\//, "")
5
+ property = property.gsub(/^#\//, "") if property
6
+
7
+ if property && fragment != "" && fragment !~ /\/$/
8
+ fragment = "#{fragment}/"
9
+ end
10
+
11
+ "#{fragment}#{property}"
12
+ end
13
+
14
+
15
+ def self.schema_path_lookup(schema, path)
16
+ if path.is_a? String
17
+ return self.schema_path_lookup(schema, path.split("/"))
18
+ end
19
+
20
+ if schema.has_key?('properties')
21
+ schema = schema['properties']
22
+ end
23
+
24
+ if path.length == 1
25
+ schema[path.first]
26
+ else
27
+ if schema[path.first]
28
+ self.schema_path_lookup(schema[path.first], path.drop(1))
29
+ else
30
+ nil
31
+ end
32
+ end
33
+ end
34
+
35
+
36
+
37
+ SCHEMA_PARSE_RULES =
38
+ [
39
+ {
40
+ :failed_attribute => ['Properties', 'IfMissing', 'ArchivesSpaceSubType'],
41
+ :pattern => /([A-Z]+: )?The property '.*?' did not contain a required property of '(.*?)'.*/,
42
+ :do => ->(msgs, message, path, type, property) {
43
+ if type && type =~ /ERROR/
44
+ msgs[:errors][fragment_join(path, property)] = ["Property is required but was missing"]
45
+ else
46
+ msgs[:warnings][fragment_join(path, property)] = ["Property was missing"]
47
+ end
48
+ }
49
+ },
50
+
51
+ {
52
+ :failed_attribute => ['ArchivesSpaceType'],
53
+ :pattern => /The property '#(.*?)' was not a well-formed date/,
54
+ :do => ->(msgs, message, path, property) {
55
+ msgs[:errors][fragment_join(path)] = ["Not a valid date"]
56
+ }
57
+ },
58
+
59
+ {
60
+ :failed_attribute => ['Pattern'],
61
+ :pattern => /The property '#\/.*?' did not match the regex '(.*?)' in schema/,
62
+ :do => ->(msgs, message, path, regexp) {
63
+ msgs[:errors][fragment_join(path)] = ["Did not match regular expression: #{regexp}"]
64
+ }
65
+ },
66
+
67
+ {
68
+ :failed_attribute => ['MinLength'],
69
+ :pattern => /The property '#\/.*?' was not of a minimum string length of ([0-9]+) in schema/,
70
+ :do => ->(msgs, message, path, length) {
71
+ msgs[:errors][fragment_join(path)] = ["Must be at least #{length} characters"]
72
+ }
73
+ },
74
+
75
+ {
76
+ :failed_attribute => ['MaxLength'],
77
+ :pattern => /The property '#\/.*?' was not of a maximum string length of ([0-9]+) in schema/,
78
+ :do => ->(msgs, message, path, length) {
79
+ msgs[:errors][fragment_join(path)] = ["Must be #{length} characters or fewer"]
80
+ }
81
+ },
82
+
83
+ {
84
+ :failed_attribute => ['MinItems'],
85
+ :pattern => /The property '#\/.*?' did not contain a minimum number of items ([0-9]+) in schema/,
86
+ :do => ->(msgs, message, path, items) {
87
+ msgs[:errors][fragment_join(path)] = ["At least #{items} item(s) is required"]
88
+ }
89
+ },
90
+
91
+ {
92
+ :failed_attribute => ['Enum'],
93
+ :pattern => /The property '#\/.*?' value "(.*?)" .*values: (.*) in schema/,
94
+ :do => ->(msgs, message, path, invalid, valid_set) {
95
+ msgs[:errors][fragment_join(path)] = ["Invalid value '#{invalid}'. Must be one of: #{valid_set}"]
96
+ }
97
+ },
98
+
99
+ {
100
+ :failed_attribute => ['ArchivesSpaceDynamicEnum'],
101
+ :pattern => /The property '#\/.*?' value "(.*?)" .*values: (.*) in schema/,
102
+ :do => ->(msgs, message, path, invalid, valid_set) {
103
+ msgs[:attribute_types][fragment_join(path)] = 'ArchivesSpaceDynamicEnum'
104
+ msgs[:errors][fragment_join(path)] = ["Invalid value '#{invalid}'. Must be one of: #{valid_set}"]
105
+ }
106
+ },
107
+ {
108
+ :failed_attribute => ['ArchivesSpaceReadOnlyDynamicEnum'],
109
+ :pattern => /The property '#\/.*?' value "(.*?)" .*values: (.*) in schema/,
110
+ :do => ->(msgs, message, path, invalid, valid_set) {
111
+ msgs[:attribute_types][fragment_join(path)] = 'ArchivesSpaceReadOnlyDynamicEnum'
112
+ msgs[:errors][fragment_join(path)] = ["Protected read-only list #{path}. Invalid value '#{invalid}'. Must be one of: #{valid_set}"]
113
+ }
114
+ },
115
+
116
+ {
117
+ :failed_attribute => ['Type', 'ArchivesSpaceType'],
118
+ :pattern => /The property '#\/.*?' of type (.*?) did not match the following type: (.*?) in schema/,
119
+ :do => ->(msgs, message, path, actual_type, desired_type) {
120
+ if actual_type !~ /JSONModel/ || message[:failed_attribute] == 'ArchivesSpaceType'
121
+ # We'll skip JSONModels because the specific problem with the
122
+ # document will have already been listed separately.
123
+
124
+ msgs[:state][fragment_join(path)] ||= []
125
+ msgs[:state][fragment_join(path)] << desired_type
126
+
127
+ if msgs[:state][fragment_join(path)].length == 1
128
+ msgs[:errors][fragment_join(path)] = ["Must be a #{desired_type} (you provided a #{actual_type})"]
129
+ else
130
+ msgs[:errors][fragment_join(path)] = ["Must be one of: #{msgs[:state][fragment_join(path)].join (", ")} (you provided a #{actual_type})"]
131
+ end
132
+ end
133
+
134
+ }
135
+ },
136
+
137
+ {
138
+ :failed_attribute => ['custom_validation'],
139
+ :pattern => /Validation failed for '(.*?)': (.*?) in schema /,
140
+ :do => ->(msgs, message, path, property, msg) {
141
+ property = (property && !property.empty?) ? property : nil
142
+ msgs[:errors][fragment_join(path, property)] = [msg]
143
+ }
144
+ },
145
+
146
+ {
147
+ :failed_attribute => ['custom_validation'],
148
+ :pattern => /Warning generated for '(.*?)': (.*?) in schema /,
149
+ :do => ->(msgs, message, path, property, msg) {
150
+ msgs[:warnings][fragment_join(path, property)] = [msg]
151
+ }
152
+ },
153
+
154
+ {
155
+ :failed_attribute => ['custom_validation'],
156
+ :pattern => /Validation error code: (.*?) in schema /,
157
+ :do => ->(msgs, message, path, error_code) {
158
+ msgs[:errors]['coded_errors'] = [error_code]
159
+ }
160
+ },
161
+
162
+
163
+ # Catch all
164
+ {
165
+ :failed_attribute => nil,
166
+ :pattern => /^(.*)$/,
167
+ :do => ->(msgs, message, path, msg) {
168
+ msgs[:errors]['unknown'] = [msg]
169
+ }
170
+ }
171
+ ]
172
+
173
+
174
+ # For a given error, find its list of sub errors.
175
+ def self.extract_suberrors(errors)
176
+ errors = Array[errors].flatten
177
+
178
+ result = errors.map do |error|
179
+ if !error[:errors]
180
+ error
181
+ else
182
+ self.extract_suberrors(error[:errors])
183
+ end
184
+ end
185
+
186
+ result.flatten
187
+ end
188
+
189
+
190
+
191
+ # Given a list of error messages produced by JSON schema validation, parse
192
+ # them into a structured format like:
193
+ #
194
+ # {
195
+ # :errors => {:attr1 => "(What was wrong with attr1)"},
196
+ # :warnings => {:attr2 => "(attr2 not quite right either)"}
197
+ # }
198
+ def self.parse_schema_messages(messages, validator)
199
+
200
+ messages = self.extract_suberrors(messages)
201
+
202
+ msgs = {
203
+ :errors => {},
204
+ :warnings => {},
205
+ # to lookup e.g., msgs[:attribute_types]['extents/0/extent_type'] => 'ArchivesSpaceDynamicEnum'
206
+ :attribute_types => {},
207
+ :state => {} # give the parse rules somewhere to store useful state for a run
208
+ }
209
+
210
+ messages.each do |message|
211
+
212
+ SCHEMA_PARSE_RULES.each do |rule|
213
+ if (rule[:failed_attribute].nil? || rule[:failed_attribute].include?(message[:failed_attribute])) and
214
+ message[:message] =~ rule[:pattern]
215
+ rule[:do].call(msgs, message, message[:fragment],
216
+ *message[:message].scan(rule[:pattern]).flatten)
217
+
218
+ break
219
+ end
220
+ end
221
+
222
+ end
223
+
224
+ msgs.delete(:state)
225
+ msgs
226
+ end
227
+
228
+
229
+ # Given a hash representing a record tree, map across the hash and this
230
+ # model's schema in lockstep.
231
+ #
232
+ # Each proc in the 'transformations' array is called with the current node
233
+ # in the record tree as its first argument, and the part of the schema
234
+ # that corresponds to it. Whatever the proc returns is used to replace
235
+ # the node in the record tree.
236
+ #
237
+ def self.map_hash_with_schema(record, schema, transformations = [])
238
+ return record if not record.is_a?(Hash)
239
+
240
+ if schema.is_a?(String)
241
+ schema = resolve_schema_reference(schema)
242
+ end
243
+
244
+ # Sometimes a schema won't specify anything other than the required type
245
+ # (like {'type' => 'object'}). If there's nothing more to check, we're
246
+ # done.
247
+ return record if !schema.has_key?("properties")
248
+
249
+
250
+ # Apply transformations to the current level of the tree
251
+ transformations.each do |transform|
252
+ record = transform.call(record, schema)
253
+ end
254
+
255
+ # Now figure out how to traverse the remainder of the tree...
256
+ result = {}
257
+
258
+ record.each do |k, v|
259
+ k = k.to_s
260
+ properties = schema['properties']
261
+
262
+ if properties.has_key?(k) && (properties[k]["type"] == "object")
263
+ result[k] = self.map_hash_with_schema(v, properties[k], transformations)
264
+
265
+ elsif v.is_a?(Array) && properties.has_key?(k) && (properties[k]["type"] == "array")
266
+
267
+ # Arrays are tricky because they can either consist of a single type, or
268
+ # a number of different types.
269
+
270
+ if properties[k]["items"]["type"].is_a?(Array)
271
+ result[k] = v.map {|elt|
272
+
273
+ if elt.is_a?(Hash)
274
+ next_schema = determine_schema_for(elt, properties[k]["items"]["type"])
275
+ self.map_hash_with_schema(elt, next_schema, transformations)
276
+ elsif elt.is_a?(Array)
277
+ raise "Nested arrays aren't supported here (yet)"
278
+ else
279
+ elt
280
+ end
281
+ }
282
+
283
+ # The array contains a single type of object
284
+ elsif properties[k]["items"]["type"] === "object"
285
+ result[k] = v.map {|elt| self.map_hash_with_schema(elt, properties[k]["items"], transformations)}
286
+ else
287
+ # Just one valid type
288
+ result[k] = v.map {|elt| self.map_hash_with_schema(elt, properties[k]["items"]["type"], transformations)}
289
+ end
290
+
291
+ elsif (v.is_a?(Hash) || v.is_a?(Array)) && (properties.has_key?(k) && properties[k]["type"].is_a?(Array))
292
+ # Multiple possible types for this single value
293
+
294
+ results = (v.is_a?(Array) ? v : [v]).map {|elt|
295
+ next_schema = determine_schema_for(elt, properties[k]["type"])
296
+ self.map_hash_with_schema(elt, next_schema, transformations)
297
+ }
298
+
299
+ result[k] = v.is_a?(Array) ? results : results[0]
300
+
301
+ elsif properties.has_key?(k) && JSONModel.parse_jsonmodel_ref(properties[k]["type"])
302
+ result[k] = self.map_hash_with_schema(v, properties[k]["type"], transformations)
303
+ else
304
+ result[k] = v
305
+ end
306
+ end
307
+
308
+ result
309
+ end
310
+
311
+
312
+
313
+ def self.is_blank?(obj)
314
+ obj.nil? || obj == "" || obj == {}
315
+ end
316
+
317
+
318
+ def self.drop_empty_elements(obj)
319
+ if obj.is_a?(Hash)
320
+ Hash[obj.map do |k, v|
321
+ v = drop_empty_elements(v)
322
+ [k, v] if !is_blank?(v)
323
+ end]
324
+ elsif obj.is_a?(Array)
325
+ obj.map {|elt| drop_empty_elements(elt)}.reject {|elt| is_blank?(elt)}
326
+ else
327
+ obj
328
+ end
329
+ end
330
+
331
+
332
+ # Drop any keys from 'hash' that aren't defined in the JSON schema.
333
+ #
334
+ # If drop_readonly is true, also drop any values where the schema has
335
+ # 'readonly' set to true. These values are produced by the system for the
336
+ # client, but are not part of the data model.
337
+ #
338
+ def self.drop_unknown_properties(hash, schema, drop_readonly = false)
339
+ fn = proc do |hash, schema|
340
+ result = {}
341
+
342
+ hash.each do |k, v|
343
+ if schema["properties"].has_key?(k.to_s) && (!drop_readonly || !schema["properties"][k.to_s]["readonly"])
344
+ result[k] = v
345
+ end
346
+ end
347
+
348
+ result
349
+ end
350
+
351
+ hash = drop_empty_elements(hash)
352
+ map_hash_with_schema(hash, schema, [fn])
353
+ end
354
+
355
+
356
+ def self.apply_schema_defaults(hash, schema)
357
+ fn = proc do |hash, schema|
358
+ result = hash.clone
359
+
360
+ schema["properties"].each do |property, definition|
361
+
362
+ if definition.has_key?("default") && !hash.has_key?(property.to_s) && !hash.has_key?(property.intern)
363
+ result[property] = definition["default"]
364
+ elsif definition['type'] == 'array' && !hash.has_key?(property.to_s) && !hash.has_key?(property.intern)
365
+ # Array values that weren't provided default to empty
366
+ result[property] = []
367
+ end
368
+
369
+ end
370
+
371
+ result
372
+ end
373
+
374
+ map_hash_with_schema(hash, schema, [fn])
375
+ end
376
+
377
+
378
+ private
379
+
380
+ def self.resolve_schema_reference(schema_reference)
381
+ # This should be a reference to a different JSONModel type. Resolve it
382
+ # and return its schema.
383
+ ref = JSONModel.parse_jsonmodel_ref(schema_reference)
384
+ raise "Invalid schema given: #{schema_reference}" if !ref
385
+
386
+ JSONModel.JSONModel(ref[0]).schema
387
+ end
388
+
389
+
390
+ def self.determine_schema_for(elt, possible_schemas)
391
+ # A number of different types. Match them up based on the value of the 'jsonmodel_type' property
392
+ schema_types = possible_schemas.map {|schema| schema.is_a?(Hash) ? schema["type"] : schema}
393
+
394
+ jsonmodel_type = elt["jsonmodel_type"]
395
+
396
+ if !jsonmodel_type
397
+ raise JSONModel::ValidationException.new(:errors => {"record" => ["Can't unambiguously match #{elt.inspect} against schema types: #{schema_types.inspect}. " +
398
+ "Resolve this by adding a 'jsonmodel_type' property to #{elt.inspect}"]})
399
+ end
400
+
401
+ next_schema = schema_types.find {|type|
402
+ (type.is_a?(String) && type.include?("JSONModel(:#{jsonmodel_type})")) ||
403
+ (type.is_a?(Hash) && type["jsonmodel_type"] === jsonmodel_type)
404
+ }
405
+
406
+ if next_schema.nil?
407
+ raise "Couldn't determine type of '#{elt.inspect}'. Must be one of: #{schema_types.inspect}"
408
+ end
409
+
410
+ next_schema
411
+ end
412
+
413
+
414
+ end
@@ -0,0 +1,342 @@
1
+ require 'json-schema'
2
+ require 'atomic'
3
+ require 'uri'
4
+ require_relative 'jsonmodel_type'
5
+ require_relative 'json_schema_concurrency_fix'
6
+ require_relative 'json_schema_utils'
7
+ require_relative 'asutils'
8
+ require_relative 'validator_cache'
9
+
10
+
11
+ module JSONModel
12
+
13
+ @@models = {}
14
+ @@custom_validations = {}
15
+ @@strict_mode = false
16
+ @@init_args = {:initialized => false}
17
+
18
+ def self.custom_validations
19
+ @@custom_validations
20
+ end
21
+
22
+ def strict_mode(val)
23
+ @@strict_mode = val
24
+ end
25
+
26
+
27
+ def self.strict_mode?
28
+ @@strict_mode
29
+ end
30
+
31
+
32
+ class ValidationException < StandardError
33
+ attr_accessor :invalid_object
34
+ attr_accessor :errors
35
+ attr_accessor :warnings
36
+ attr_accessor :attribute_types
37
+ attr_accessor :import_context
38
+ attr_accessor :object_context
39
+
40
+ def initialize(opts)
41
+ @invalid_object = opts[:invalid_object]
42
+ @errors = opts[:errors]
43
+ @import_context = opts[:import_context]
44
+ @object_context = opts[:object_context]
45
+ @attribute_types = opts[:attribute_types]
46
+ end
47
+
48
+ def to_s
49
+ msg = { :errors => @errors }
50
+ msg[:import_context] = @import_context unless @import_context.nil?
51
+ msg[:object_context] = @object_context unless @object_context.nil?
52
+ "#<:ValidationException: #{msg.inspect}>"
53
+ end
54
+ end
55
+
56
+
57
+ def self.JSONModel(source)
58
+ if !@@models.has_key?(source.to_s)
59
+ load_schema(source.to_s)
60
+ end
61
+
62
+ @@models[source.to_s] or raise "JSONModel not found for #{source}"
63
+ end
64
+
65
+
66
+ def JSONModel(source)
67
+ JSONModel.JSONModel(source)
68
+ end
69
+
70
+
71
+ # Yield all known JSONModel classes
72
+ def models
73
+ @@models
74
+ end
75
+
76
+
77
+ def self.repository_for(reference)
78
+ if reference =~ /^(\/repositories\/[0-9]+)\//
79
+ return $1
80
+ else
81
+ return nil
82
+ end
83
+ end
84
+
85
+
86
+ # Parse a URI reference like /repositories/123/archival_objects/500 into
87
+ # {:id => 500, :type => :archival_object}
88
+ def self.parse_reference(reference, opts = {})
89
+ @@models.each do |type, model|
90
+ id = model.id_for(reference, opts, true)
91
+ if id
92
+ return {:id => id, :type => type, :repository => repository_for(reference)}
93
+ end
94
+ end
95
+
96
+ nil
97
+ end
98
+
99
+
100
+ def self.destroy_model(type)
101
+ @@models.delete(type.to_s)
102
+ end
103
+
104
+
105
+ def self.schema_src(schema_name)
106
+
107
+ if schema_name.to_s !~ /\A[0-9A-Za-z_-]+\z/
108
+ raise "Invalid schema name: #{schema_name}"
109
+ end
110
+
111
+ [*ASUtils.find_local_directories('schemas'),
112
+ File.join(File.dirname(__FILE__), "schemas")].each do |dir|
113
+
114
+ schema = File.join(dir, "#{schema_name}.rb")
115
+
116
+ if File.exists?(schema)
117
+ return File.open(schema).read
118
+ end
119
+ end
120
+
121
+ nil
122
+ end
123
+
124
+
125
+ def self.allow_unmapped_enum_value(val, magic_value = 'other_unmapped')
126
+ if val.is_a? Array
127
+ val.each { |elt| allow_unmapped_enum_value(elt) }
128
+ elsif val.is_a? Hash
129
+ val.each do |k, v|
130
+ if k == 'enum'
131
+ v << magic_value
132
+ else
133
+ allow_unmapped_enum_value(v)
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+
140
+ def self.load_schema(schema_name)
141
+ if not @@models[schema_name]
142
+
143
+ old_verbose = $VERBOSE
144
+ $VERBOSE = nil
145
+ src = schema_src(schema_name)
146
+
147
+ return if !src
148
+
149
+ entry = eval(src)
150
+ $VERBOSE = old_verbose
151
+
152
+ parent = entry[:schema]["parent"]
153
+ if parent
154
+ load_schema(parent)
155
+
156
+ base = @@models[parent].schema["properties"].clone
157
+ properties = ASUtils.deep_merge(base, entry[:schema]["properties"])
158
+
159
+ # Maybe we'll eventually want the version of a schema to be
160
+ # automatically set to max(my_version, parent_version), but for now...
161
+ if entry[:schema]["version"] < @@models[parent].schema_version
162
+ raise ("Can't inherit from a JSON schema whose version is newer than ours " +
163
+ "(our (#{schema_name}) version: #{entry[:schema]['version']}; " +
164
+ "parent (#{parent}) version: #{@@models[parent].schema_version})")
165
+ end
166
+
167
+ entry[:schema]["properties"] = properties
168
+ end
169
+
170
+ # All records have a lock_version property that we use for optimistic concurrency control.
171
+ entry[:schema]["properties"]["lock_version"] = {"type" => ["integer", "string"], "required" => false}
172
+
173
+ # All records must indicate their model type
174
+ entry[:schema]["properties"]["jsonmodel_type"] = {"type" => "string", "ifmissing" => "error"}
175
+
176
+ # All records have audit fields
177
+ entry[:schema]["properties"]["created_by"] = {"type" => "string", "readonly" => true}
178
+ entry[:schema]["properties"]["last_modified_by"] = {"type" => "string", "readonly" => true}
179
+ entry[:schema]["properties"]["user_mtime"] = {"type" => "date-time", "readonly" => true}
180
+ entry[:schema]["properties"]["system_mtime"] = {"type" => "date-time", "readonly" => true}
181
+ entry[:schema]["properties"]["create_time"] = {"type" => "date-time", "readonly" => true}
182
+
183
+ # Records may include a reference to the repository that contains them
184
+ entry[:schema]["properties"]["repository"] ||= {
185
+ "type" => "object",
186
+ "subtype" => "ref",
187
+ "readonly" => "true",
188
+ "properties" => {
189
+ "ref" => {
190
+ "type" => "JSONModel(:repository) uri",
191
+ "ifmissing" => "error",
192
+ "readonly" => "true"
193
+ },
194
+ "_resolved" => {
195
+ "type" => "object",
196
+ "readonly" => "true"
197
+ }
198
+ }
199
+ }
200
+
201
+
202
+ if @@init_args && @@init_args[:allow_other_unmapped]
203
+ allow_unmapped_enum_value(entry[:schema]['properties'])
204
+ end
205
+
206
+ ASUtils.find_local_directories("schemas/#{schema_name}_ext.rb").
207
+ select {|path| File.exists?(path)}.
208
+ each do |schema_extension|
209
+ entry[:schema]['properties'] = ASUtils.deep_merge(entry[:schema]['properties'],
210
+ eval(File.open(schema_extension).read))
211
+ end
212
+
213
+ self.create_model_for(schema_name, entry[:schema])
214
+ end
215
+ end
216
+
217
+
218
+ def self.init(opts = {})
219
+
220
+ @@init_args ||= nil
221
+
222
+ # Skip initialisation if this model has already been loaded.
223
+ if @@init_args[:initialized]
224
+ return true
225
+ else
226
+ @@init_args[:initialized] = true
227
+ end
228
+
229
+ if opts.has_key?(:strict_mode)
230
+ @@strict_mode = true
231
+ end
232
+
233
+ @@init_args = opts
234
+
235
+ if !opts.has_key?(:enum_source)
236
+ if opts[:client_mode]
237
+ require_relative 'jsonmodel_client'
238
+ opts[:enum_source] = JSONModel::Client::EnumSource.new
239
+ else
240
+ raise "Required JSONModel.init arg :enum_source was missing"
241
+ end
242
+ end
243
+
244
+ # Load all JSON schemas from the schemas subdirectory
245
+ # Create a model class for each one.
246
+ Dir.glob(File.join(File.dirname(__FILE__),
247
+ "schemas",
248
+ "*.rb")).sort.each do |schema|
249
+ schema_name = File.basename(schema, ".rb")
250
+ load_schema(schema_name)
251
+ end
252
+
253
+ require_relative "validations"
254
+
255
+ # For dynamic enums, automatically slot in the 'other_unmapped' string as an allowable value
256
+ if @@init_args[:allow_other_unmapped]
257
+ enum_wrapper = Struct.new(:enum_source).new(@@init_args[:enum_source])
258
+
259
+ def enum_wrapper.valid?(name, value)
260
+ value == 'other_unmapped' || enum_source.valid?(name, value)
261
+ end
262
+
263
+ def enum_wrapper.editable?(name)
264
+ enum_source.editable?(name)
265
+ end
266
+
267
+ def enum_wrapper.values_for(name)
268
+ enum_source.values_for(name) + ['other_unmapped']
269
+ end
270
+
271
+ def enum_wrapper.default_value_for(name)
272
+ enum_source.default_value_for(name)
273
+ end
274
+
275
+ @@init_args[:enum_source] = enum_wrapper
276
+ end
277
+
278
+ true
279
+
280
+ rescue
281
+ # If anything went wrong we're not initialised.
282
+ @@init_args = nil
283
+
284
+ raise $!
285
+ end
286
+
287
+
288
+ def self.enum_values(name)
289
+ @@init_args[:enum_source].values_for(name)
290
+ end
291
+
292
+
293
+ def self.enum_default_value(name)
294
+ @@init_args[:enum_source].default_value_for(name)
295
+ end
296
+
297
+
298
+ def self.client_mode?
299
+ @@init_args[:client_mode]
300
+ end
301
+
302
+
303
+ def self.parse_jsonmodel_ref(ref)
304
+ if ref.is_a? String and ref =~ /JSONModel\(:([a-zA-Z_\-]+)\) (.*)/
305
+ [$1.intern, $2]
306
+ else
307
+ nil
308
+ end
309
+ end
310
+
311
+
312
+ protected
313
+
314
+
315
+ def self.clean_data(data)
316
+ data = ASUtils.keys_as_strings(data) if data.is_a?(Hash)
317
+ data = ASUtils.jsonmodels_to_hashes(data)
318
+
319
+ data
320
+ end
321
+
322
+
323
+ # Create and return a new JSONModel class called 'type', based on the
324
+ # JSONSchema 'schema'
325
+ def self.create_model_for(type, schema)
326
+
327
+ cls = Class.new(JSONModelType)
328
+ cls.init(type, schema, Array(@@init_args[:mixins]))
329
+
330
+ @@models[type] = cls
331
+ end
332
+
333
+
334
+ def self.init_args
335
+ @@init_args
336
+ end
337
+
338
+ end
339
+
340
+
341
+ # Custom JSON schema validations
342
+ require_relative 'archivesspace_json_schema'