osullivan 0.0.2

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