osullivan 0.0.2

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 (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +2 -0
  6. data/LICENSE +23 -0
  7. data/README.md +166 -0
  8. data/Rakefile +12 -0
  9. data/VERSION +1 -0
  10. data/lib/active_support/ordered_hash.rb +147 -0
  11. data/lib/iiif/hash_behaviours.rb +150 -0
  12. data/lib/iiif/presentation.rb +25 -0
  13. data/lib/iiif/presentation/abstract_resource.rb +75 -0
  14. data/lib/iiif/presentation/annotation.rb +25 -0
  15. data/lib/iiif/presentation/annotation_list.rb +28 -0
  16. data/lib/iiif/presentation/canvas.rb +45 -0
  17. data/lib/iiif/presentation/collection.rb +29 -0
  18. data/lib/iiif/presentation/image_resource.rb +115 -0
  19. data/lib/iiif/presentation/layer.rb +34 -0
  20. data/lib/iiif/presentation/manifest.rb +39 -0
  21. data/lib/iiif/presentation/range.rb +32 -0
  22. data/lib/iiif/presentation/resource.rb +21 -0
  23. data/lib/iiif/presentation/sequence.rb +35 -0
  24. data/lib/iiif/service.rb +418 -0
  25. data/osullivan.gemspec +27 -0
  26. data/spec/fixtures/manifests/complete_from_spec.json +171 -0
  27. data/spec/fixtures/manifests/minimal.json +40 -0
  28. data/spec/fixtures/manifests/service_only.json +11 -0
  29. data/spec/fixtures/vcr_cassettes/pul_loris_cassette.json +159 -0
  30. data/spec/integration/iiif/presentation/image_resource_spec.rb +123 -0
  31. data/spec/integration/iiif/service_spec.rb +211 -0
  32. data/spec/spec_helper.rb +104 -0
  33. data/spec/unit/active_support/ordered_hash_spec.rb +155 -0
  34. data/spec/unit/iiif/hash_behaviours_spec.rb +569 -0
  35. data/spec/unit/iiif/presentation/abstract_resource_spec.rb +133 -0
  36. data/spec/unit/iiif/presentation/annotation_list_spec.rb +7 -0
  37. data/spec/unit/iiif/presentation/annotation_spec.rb +7 -0
  38. data/spec/unit/iiif/presentation/canvas_spec.rb +40 -0
  39. data/spec/unit/iiif/presentation/collection_spec.rb +54 -0
  40. data/spec/unit/iiif/presentation/image_resource_spec.rb +13 -0
  41. data/spec/unit/iiif/presentation/layer_spec.rb +38 -0
  42. data/spec/unit/iiif/presentation/manifest_spec.rb +89 -0
  43. data/spec/unit/iiif/presentation/range_spec.rb +43 -0
  44. data/spec/unit/iiif/presentation/resource_spec.rb +16 -0
  45. data/spec/unit/iiif/presentation/sequence_spec.rb +110 -0
  46. data/spec/unit/iiif/presentation/shared_examples/abstract_resource_only_keys.rb +43 -0
  47. data/spec/unit/iiif/presentation/shared_examples/any_type_keys.rb +33 -0
  48. data/spec/unit/iiif/presentation/shared_examples/array_only_keys.rb +44 -0
  49. data/spec/unit/iiif/presentation/shared_examples/int_only_keys.rb +49 -0
  50. data/spec/unit/iiif/presentation/shared_examples/string_only_keys.rb +29 -0
  51. data/spec/unit/iiif/service_spec.rb +10 -0
  52. metadata +246 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b50c5e03818f7b4152010d37813f230abe570c72
4
+ data.tar.gz: 594ca02320dd7e3ed0412784476dea78b732fb8a
5
+ SHA512:
6
+ metadata.gz: 32172a98698cd30d59178385e6098919f9053154ea6f77b36df0e4c114a2a53aa59e831241c7ab608fc75c3423d05ca7bfbbbb99430cacb0686de17fb50638af
7
+ data.tar.gz: 5437e7a202ceda204b49b4f32ad1fe4d3eb3cc70a85abb9e22f2a1c14f37219596a40a0562d27fab9689846a0c8d51c0143e5996f0e8e4b643e1b56b3e70fa3e
@@ -0,0 +1,4 @@
1
+ coverage/
2
+ pkg/
3
+ Gemfile.lock
4
+
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --require spec_helper
3
+ --format documentation
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.0
4
+
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2014, Jon Stroop
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,166 @@
1
+ # OSullivan
2
+
3
+ [![Build Status](https://travis-ci.org/jpstroop/osullivan.svg?branch=development)](https://travis-ci.org/jpstroop/osullivan) [![Coverage Status](https://coveralls.io/repos/jpstroop/osullivan/badge.png?branch=development)](https://coveralls.io/r/jpstroop/osullivan?branch=development)
4
+
5
+ ## Building New Objects
6
+
7
+ There is (or will be) a class for all types in [IIIF Presentation API Spec](http://iiif.io/api/presentation/2.0/).
8
+
9
+ After you've installed the gem (not yet published. Clone and do `rake:install`):
10
+
11
+ ```ruby
12
+ require 'iiif/presentation'
13
+
14
+ seed = {
15
+ '@id' => 'http://example.com/manifest',
16
+ 'label' => 'My Manifest'
17
+ }
18
+ # Any options you add are added to the object
19
+ manifest = IIIF::Presentation::Manifest.new(seed)
20
+
21
+ canvas = IIIF::Presentation::Canvas.new()
22
+ # All classes act like `ActiveSupport::OrderedHash`es, for the most part.
23
+ # Use `[]=` to set JSON-LD properties...
24
+ canvas['@id'] = 'http://example.com/canvas'
25
+ # ...but there are also accessors and mutators for the properties mentioned in
26
+ # the spec
27
+ canvas.width = 10
28
+ canvas.height = 20
29
+ canvas.label = 'My Canvas'
30
+
31
+ oc = IIIF::Presentation::Resource.new('@id' => 'http://example.com/content')
32
+ canvas.other_content << oc
33
+
34
+ manifest.sequences << canvas
35
+
36
+ puts manifest.to_json(pretty: true)
37
+ ```
38
+
39
+ Methods are generated dynamically, which means `#methods` is your friend:
40
+
41
+ ```ruby
42
+ manifest = IIIF::Presentation::Manifest.new()
43
+ puts manifest.methods(false)
44
+ > label=
45
+ > label
46
+ > description=
47
+ > description
48
+ > thumbnail=
49
+ > thumbnail
50
+ > attribution=
51
+ > attribution
52
+ > viewing_hint=
53
+ > viewingHint=
54
+ > viewing_hint
55
+ > viewingHint
56
+ [...]
57
+ ```
58
+
59
+ Note that multi-word properties are implemented as snake_case (because this is
60
+ Ruby), but is serialized as camelCase. There are camelCase aliases for these.
61
+
62
+ ```ruby
63
+ manifest = IIIF::Presentation::Manifest.new()
64
+ manifest.viewing_hint = 'paged'
65
+ puts manifest.to_json(pretty: true, force: true) # force: true skips validations
66
+
67
+ > {
68
+ > "@context": "http://iiif.io/api/presentation/2/context.json",
69
+ > "@type": "sc:Manifest",
70
+ > "viewingHint": "paged"
71
+ > }
72
+
73
+ ```
74
+
75
+ ## Parsing Existing Objects
76
+
77
+ Use `IIIF::Service#parse`. It will figure out what the object
78
+ should be, based on `@type`, and fall back to `ActiveSupport::OrderedHash` when
79
+ it can't e.g.:
80
+
81
+ ```ruby
82
+ seed = '{
83
+ "@context": "http://iiif.io/api/presentation/2/context.json",
84
+ "@id": "http://example.com/manifest",
85
+ "@type": "sc:Manifest",
86
+ "label": "My Manifest",
87
+ "service": {
88
+ "@context": "http://iiif.io/api/image/2/context.json",
89
+ "@id":"http://www.example.org/images/book1-page1",
90
+ "profile":"http://iiif.io/api/image/2/profiles/level2.json"
91
+ },
92
+ "seeAlso": {
93
+ "@id": "http://www.example.org/library/catalog/book1.marc",
94
+ "format": "application/marc"
95
+ },
96
+ "sequences": [
97
+ {
98
+ "@id":"http://www.example.org/iiif/book1/sequence/normal",
99
+ "@type":"sc:Sequence",
100
+ "label":"Current Page Order",
101
+ "viewingDirection":"left-to-right",
102
+ "viewingHint":"paged",
103
+ "startCanvas": "http://www.example.org/iiif/book1/canvas/p2",
104
+ "canvases": [
105
+ {
106
+ "@id": "http://example.com/canvas",
107
+ "@type": "sc:Canvas",
108
+ "width": 10,
109
+ "height": 20,
110
+ "label": "My Canvas",
111
+ "otherContent": [
112
+ {
113
+ "@id": "http://example.com/content",
114
+ "@type":"sc:AnnotationList",
115
+ "motivation": "sc:painting"
116
+ }
117
+ ]
118
+ }
119
+ ]
120
+ }
121
+ ]
122
+ }'
123
+
124
+ obj = IIIF::Service.parse(seed) # can also be a file path or a Hash
125
+ puts obj.class
126
+ puts obj.see_also.class
127
+
128
+ > IIIF::Presentation::Manifest
129
+ > ActiveSupport::OrderedHash
130
+ ```
131
+
132
+ ## Validation and Exceptions
133
+
134
+ This is work in progress. Right now exceptions are generally raised when you
135
+ try to set something to a type it should never be:
136
+
137
+ ```ruby
138
+ manifest = IIIF::Presentation::Manifest.new
139
+ manifest.sequences = 'quux'
140
+
141
+ > [...] sequences must be an Array. (IIIF::Presentation::IllegalValueError)
142
+ ```
143
+
144
+ and also if any required properties are missing when calling `to_json`
145
+
146
+ ```ruby
147
+ canvas = IIIF::Presentation::Canvas.new('@id' => 'http://example.com/canvas')
148
+ puts canvas.to_json(pretty: true)
149
+
150
+ > A(n) width is required for each IIIF::Presentation::Canvas (IIIF::Presentation::MissingRequiredKeyError)
151
+ ```
152
+
153
+ but you can skip this validation by adding `force: true`:
154
+
155
+ ```ruby
156
+ canvas = IIIF::Presentation::Canvas.new('@id' => 'http://example.com/canvas')
157
+ puts canvas.to_json(pretty: true, force: true)
158
+
159
+ > {
160
+ > "@context": "http://iiif.io/api/presentation/2/context.json",
161
+ > "@id": "http://example.com/canvas",
162
+ > "@type": "sc:Canvas"
163
+ > }
164
+ ```
165
+ This all needs a bit of tidying up, finishing, and refactoring, so expect it to
166
+ change.
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require "bundler/gem_tasks"
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task :ci do
9
+ Rake::Task['spec'].invoke
10
+ end
11
+
12
+ task default: :ci
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.2
@@ -0,0 +1,147 @@
1
+ require 'active_support/inflector'
2
+ require 'active_support/ordered_hash'
3
+
4
+ module ActiveSupport
5
+ class OrderedHash < ::Hash
6
+
7
+ # Insert a new key and value at the suppplied index.
8
+ #
9
+ # Note that this is slightly different from Array#insert in that new
10
+ # entries must be added one at a time, i.e. insert(n, k, v, k, v...) is
11
+ # not supported.
12
+ #
13
+ # @param [Integer] index
14
+ # @param [Object] key
15
+ # @param [Object] value
16
+ def insert(index, key, value)
17
+ tmp = ActiveSupport::OrderedHash.new
18
+ index = self.length + 1 + index if index < 0
19
+ if index < 0
20
+ m = "Index #{index} is too small for current length (#{length})"
21
+ raise IndexError, m
22
+ end
23
+ if index > 0
24
+ i=0
25
+ self.each do |k,v|
26
+ tmp[k] = v
27
+ self.delete(k)
28
+ i+=1
29
+ break if i == index
30
+ end
31
+ end
32
+ tmp[key] = value
33
+ tmp.merge!(self) # copy the remaining to tmp
34
+ self.clear # start over...
35
+ self.merge!(tmp) # now put them all back
36
+ self
37
+ end
38
+
39
+ # Insert a key and value before an existing key or the first entry for'
40
+ # which the supplied block evaluates to true. The block takes precendence
41
+ # over the supplied key.
42
+ # Options:'
43
+ # * :existing_key (default: nil). If nil or not supplied then a block is required.
44
+ # * :new_key (required)
45
+ # * :value (required)
46
+ # @raise KeyError if the supplied existing key is not found, the new
47
+ # key exists, or the block never evaluates to true.
48
+ def insert_before(hsh, &block)
49
+ existing_key = hsh.fetch(:existing_key, nil)
50
+ new_key = hsh[:new_key]
51
+ value = hsh[:value]
52
+ if block_given?
53
+ self.insert_here(0, new_key, value, &block)
54
+ else
55
+ self.insert_here(0, new_key, value, existing_key)
56
+ end
57
+ end
58
+
59
+ # Insert a key and value after an existing key or the first entry for'
60
+ # which the supplied block evaluates to true. The block takes precendence
61
+ # over the supplied key.
62
+ # Options:'
63
+ # * :existing_key (default: nil). If nil or not supplied then a block is required.
64
+ # * :new_key (required)
65
+ # * :value (required)
66
+ # @raise KeyError if the supplied existing key is not found, the new
67
+ # key exists, or the block never evaluates to true.
68
+ def insert_after(hsh, &block)
69
+ existing_key = hsh.fetch(:existing_key, nil)
70
+ new_key = hsh[:new_key]
71
+ value = hsh[:value]
72
+ if block_given?
73
+ self.insert_here(1, new_key, value, &block)
74
+ else
75
+ self.insert_here(1, new_key, value, existing_key)
76
+ end
77
+ end
78
+
79
+ # Delete any keys that are empty arrays
80
+ def remove_empties
81
+ self.keys.each do |key|
82
+ if (self[key].kind_of?(Array) && self[key].empty?) || self[key].nil?
83
+ self.delete(key)
84
+ end
85
+ end
86
+ end
87
+
88
+ # Covert snake_case keys to camelCase
89
+ def camelize_keys
90
+ self.keys.each_with_index do |key, i|
91
+ if key != key.camelize(:lower)
92
+ self.insert(i, key.camelize(:lower), self[key])
93
+ self.delete(key)
94
+ end
95
+ end
96
+ self
97
+ end
98
+
99
+ # Covert camelCase keys to snake_case
100
+ def snakeize_keys
101
+ self.keys.each_with_index do |key, i|
102
+ if key != key.underscore
103
+ self.insert(i, key.underscore, self[key])
104
+ self.delete(key)
105
+ end
106
+ end
107
+ self
108
+ end
109
+
110
+
111
+ # Prepends an entry to the front of the object.
112
+ # Note that this is slightly different from Array#unshift in that new
113
+ # entries must be added one at a time, i.e. unshift([k,v],[k,v],...) is
114
+ # not currently supported.
115
+ def unshift k,v
116
+ self.insert(0, k, v)
117
+ self
118
+ end
119
+
120
+ protected
121
+ def insert_here(where, new_key, value, existing_key=nil, &block)
122
+ idx = nil
123
+ if block_given?
124
+ self.each_with_index do |(k,v), i|
125
+ if yield(k, v)
126
+ idx = i
127
+ break
128
+ end
129
+ end
130
+ if idx.nil?
131
+ raise KeyError, "Supplied block never evaluates to true"
132
+ end
133
+ else
134
+ unless self.has_key?(existing_key)
135
+ raise KeyError, "Existing key '#{existing_key}' does not exist"
136
+ end
137
+ if self.has_key?(new_key)
138
+ raise KeyError, "Supplied new key '#{new_key}' already exists"
139
+ end
140
+ idx = self.keys.index(existing_key) + where
141
+ end
142
+ self.insert(idx, new_key, value)
143
+ self
144
+ end
145
+
146
+ end
147
+ end
@@ -0,0 +1,150 @@
1
+ require 'forwardable'
2
+
3
+ module IIIF
4
+ module HashBehaviours
5
+ extend Forwardable
6
+
7
+ # TODO:
8
+ # * reject
9
+ # * replace
10
+
11
+ def_delegators :@data, :[], :[]=, :camelize_keys, :delete, :empty?,
12
+ :fetch, :has_key?, :has_value?, :include?, :insert, :insert_after,
13
+ :insert_before, :key, :key?, :keys, :length, :member?, :shift, :size,
14
+ :snakeize_keys, :store, :unshift, :value?, :values
15
+
16
+
17
+ ###
18
+ # Methods that take a block and should return an instance (self or a new'
19
+ # instance) have been overridden to do so, rather than an'
20
+ # ActiveSupport::OrderedHash based on the internal hash
21
+
22
+ SIMPLE_SELF_RETURNERS = %w[delete_if each each_key each_value keep_if]
23
+
24
+ SIMPLE_SELF_RETURNERS.each do |method_name|
25
+ define_method(method_name) do |*arg, &block|
26
+ unless block.nil? # block_given? doesn't seem to work in this context
27
+ @data.send(method_name, *arg, &block)
28
+ return self
29
+ else
30
+ @data.send(method_name)
31
+ end
32
+ end
33
+ end
34
+
35
+ # Clear is the only method that returns self but doesn't accept a block
36
+ def clear
37
+ @data.clear
38
+ return self
39
+ end
40
+
41
+ # Returns a new instance of this class containing the contents of'
42
+ # another_obj. The argument can be any object that implements two
43
+ # methods:
44
+ #
45
+ # obj.each { |k,v| block }
46
+ # obj.has_key?
47
+ #
48
+ # If no block is specified, the value for entries with duplicate keys'
49
+ # will be those of the argument, but at the index of the original; all'
50
+ # other entries will be appended to the end.
51
+ #
52
+ # If a block is specified the value for each duplicate key is determined'
53
+ # by calling the block with the key, its value in hsh and its value in'
54
+ # another_obj.
55
+ def merge another_obj
56
+ new_instance = self.class.new
57
+ # self.clone # Would this be better? What happens to other attributes of the class?
58
+ if block_given?
59
+ self.each do |k,v|
60
+ if another_obj.has_key? k
61
+ new_instance[k] = yield(k, self[k], another_obj[k])
62
+ else
63
+ new_instance[k] = v
64
+ end
65
+ end
66
+ else
67
+ self.each { |k,v| new_instance[k] = v }
68
+ another_obj.each { |k,v| new_instance[k] = v }
69
+ end
70
+ new_instance
71
+ end
72
+
73
+ # Adds the entries from another obj to this one. The argument can be any
74
+ # object that implements two methods:
75
+ #
76
+ # obj.each { |k,v| block }
77
+ # obj.has_key?
78
+ #
79
+ # If no block is specified, the value for entries with duplicate keys'
80
+ # will be those of the argument, but at the index of the original; all'
81
+ # other entries will be appended to the end.
82
+ #
83
+ # If a block is specified the value for each duplicate key is determined'
84
+ # by calling the block with the key, its value in hsh and its value in'
85
+ # another_obj.
86
+ def merge! another_obj
87
+ if block_given?
88
+ self.each do |k,v|
89
+ if another_obj.has_key? k
90
+ self[k] = yield(k, self[k], another_obj[k])
91
+ else
92
+ self[k] = v
93
+ end
94
+ end
95
+ else
96
+ self.each { |k,v| self[k] = v }
97
+ another_obj.each { |k,v| self[k] = v }
98
+ end
99
+ self
100
+ end
101
+ alias update merge!
102
+
103
+ # Deletes entries for which the supplied block evaluates to true.
104
+ # Equivalent to #delete_if, but returns nil if there were no changes
105
+ def reject!
106
+ if block_given?
107
+ return_nil = true
108
+ @data.each do |k, v|
109
+ if yield(k, v)
110
+ @data.delete(k)
111
+ return_nil = false
112
+ end
113
+ end
114
+ return return_nil ? nil : self
115
+ else
116
+ return self.data.reject!
117
+ end
118
+ end
119
+
120
+ # Returns a new instance consisting of entries for which the block returns
121
+ # true. Not that an enumerator is not available for the OrderedHash'
122
+ # implementation
123
+ def select
124
+ new_instance = self.class.new
125
+ if block_given?
126
+ @data.each { |k,v| new_instance.data[k] = v if yield(k,v) }
127
+ end
128
+ return new_instance
129
+ end
130
+
131
+ # Deletes entries for which the supplied block evaluates to false.
132
+ # Equivalent to Hash#keep_if, but returns nil if no changes were made.
133
+ def select!
134
+ if block_given?
135
+ return_nil = true
136
+ @data.each do |k,v|
137
+ unless yield(k,v)
138
+ @data.delete(k)
139
+ return_nil = false
140
+ end
141
+ end
142
+ return nil if return_nil
143
+ end
144
+ self
145
+ end
146
+
147
+ end
148
+
149
+ end
150
+