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,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'