jsonapi 0.1.1.beta1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b9684f22cfc478709e3961ccde7d5c528293587c
4
+ data.tar.gz: 62b48f99bb23746b67fc0e5e42f864d3df8d4116
5
+ SHA512:
6
+ metadata.gz: f77d8cb69325b8186786e181515c0ce7a30fd6f63fcf5ae22c65ac4e5167c87128a8124e87f63ef4115275f36bd9d79151198c200ec31230917aa2da51b2cd97
7
+ data.tar.gz: fad6e70f1a1efedea35600adb16784de57c100cfcaab3d18f26ce80c1eb3d06a3cbb22fae5c7cf5da4429ed98a5cbf1576f4812923b64172e13ea566d830a611
data/.gitignore ADDED
@@ -0,0 +1,36 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ ## Specific to RubyMotion:
14
+ .dat*
15
+ .repl_history
16
+ build/
17
+
18
+ ## Documentation cache and generated files:
19
+ /.yardoc/
20
+ /_yardoc/
21
+ /doc/
22
+ /rdoc/
23
+
24
+ ## Environment normalization:
25
+ /.bundle/
26
+ /vendor/bundle
27
+ /lib/bundler/man/
28
+
29
+ # for a library or gem, you might want to ignore these files since the code is
30
+ # intended to run in multiple environments; otherwise, check them in:
31
+ # Gemfile.lock
32
+ # .ruby-version
33
+ # .ruby-gemset
34
+
35
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
36
+ .rvmrc
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ sudo: false
3
+ rvm:
4
+ - 2.1
5
+ - 2.2
6
+ - 2.3.0
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,48 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ jsonapi (0.1.1.beta1)
5
+ activesupport (>= 3.0)
6
+ json (~> 1.8)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activesupport (4.2.6)
12
+ i18n (~> 0.7)
13
+ json (~> 1.7, >= 1.7.7)
14
+ minitest (~> 5.1)
15
+ thread_safe (~> 0.3, >= 0.3.4)
16
+ tzinfo (~> 1.1)
17
+ diff-lcs (1.2.5)
18
+ i18n (0.7.0)
19
+ json (1.8.3)
20
+ minitest (5.8.4)
21
+ rake (11.1.2)
22
+ rspec (3.4.0)
23
+ rspec-core (~> 3.4.0)
24
+ rspec-expectations (~> 3.4.0)
25
+ rspec-mocks (~> 3.4.0)
26
+ rspec-core (3.4.4)
27
+ rspec-support (~> 3.4.0)
28
+ rspec-expectations (3.4.0)
29
+ diff-lcs (>= 1.2.0, < 2.0)
30
+ rspec-support (~> 3.4.0)
31
+ rspec-mocks (3.4.1)
32
+ diff-lcs (>= 1.2.0, < 2.0)
33
+ rspec-support (~> 3.4.0)
34
+ rspec-support (3.4.1)
35
+ thread_safe (0.3.5)
36
+ tzinfo (1.2.2)
37
+ thread_safe (~> 0.1)
38
+
39
+ PLATFORMS
40
+ ruby
41
+
42
+ DEPENDENCIES
43
+ jsonapi!
44
+ rake (>= 0.9)
45
+ rspec (~> 3.4)
46
+
47
+ BUNDLED WITH
48
+ 1.11.2
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Lucas Hosseini
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # jsonapi
2
+ Ruby gem for parsing/validating [JSON API](http://jsonapi.org) documents.
3
+
4
+ ## Installation
5
+
6
+ Add the following to your application's Gemfile:
7
+ ```ruby
8
+ gem 'jsonapi'
9
+ ```
10
+ And then execute:
11
+ ```
12
+ $ bundle
13
+ ```
14
+ Or install it manually as:
15
+ ```
16
+ $ gem install jsonapi
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ First, `require` the gem.
22
+ ```ruby
23
+ require 'jsonapi'
24
+ ```
25
+
26
+ Then, parse a JSON API document:
27
+ ```ruby
28
+ document = JSONAPI.parse(hash_or_json_string)
29
+ ```
30
+
31
+ or an [include directive](http://jsonapi.org/format/#fetching-includes):
32
+ ```ruby
33
+ include_directive = JSONAPI::IncludeDirective.new(include_args)
34
+ ```
35
+
36
+ ## Examples
37
+
38
+ ```ruby
39
+ document = JSONAPI.parse(json_document)
40
+ # Should the document be invalid, the parse method would fail with an
41
+ # InvalidDocument error.
42
+
43
+ document.data.to_activerecord_hash
44
+ # => {
45
+ # id: '1',
46
+ # title: 'JSON API paints my bikeshed!',
47
+ # author_id: '9',
48
+ # comment_ids: ['5', '12']
49
+ # }
50
+
51
+ document.data.links.defined?(:self)
52
+ # => true
53
+ document.data.links.self.value
54
+ # => 'http://example.com/articles/1'
55
+ document.data.attributes.keys
56
+ # => ['title']
57
+ document.data.attributes.defined?(:title)
58
+ # => true
59
+ document.data.attributes.title
60
+ # => 'JSON API paints my bikeshed!'
61
+ document.data.relationships.keys
62
+ # => ['author', 'comments']
63
+ document.data.relationships.defined?(:author)
64
+ # => true
65
+ document.data.relationships.author.collection?
66
+ # => false
67
+ document.data.relationships.author.data.id
68
+ # => 9
69
+ document.data.relationships.author.data.type
70
+ # => 'people'
71
+ document.data.relationships.author.links.defined?(:self)
72
+ # => true
73
+ document.data.relationships.author.links.self.value
74
+ # => 'http://example.com/articles/1/relationships/author'
75
+ document.data.relationships.defined?(:comments)
76
+ # => true
77
+ document.data.relationships.comments.collection?
78
+ # => true
79
+ document.data.relationships.comments.data.size
80
+ # => 2
81
+ document.data.relationships.comments.data[0].id
82
+ # => 5
83
+ document.data.relationships.comments.data[0].type
84
+ # => 'comments'
85
+ document.data.relationships.comments.links.defined?(:self)
86
+ # => true
87
+ document.data.relationships.comments.links.self.value
88
+ # => 'http://example.com/articles/1/relationships/comments'
89
+
90
+ # for the following document_hash
91
+ document_hash = {
92
+ 'data' =>
93
+ {
94
+ 'type' => 'articles',
95
+ 'id' => '1',
96
+ 'attributes' => {
97
+ 'title' => 'JSON API paints my bikeshed!'
98
+ },
99
+ 'links' => {
100
+ 'self' => 'http://example.com/articles/1'
101
+ },
102
+ 'relationships' => {
103
+ 'author' => {
104
+ 'links' => {
105
+ 'self' => 'http://example.com/articles/1/relationships/author',
106
+ 'related' => 'http://example.com/articles/1/author'
107
+ },
108
+ 'data' => { 'type' => 'people', 'id' => '9' }
109
+ },
110
+ 'comments' => {
111
+ 'links' => {
112
+ 'self' => 'http://example.com/articles/1/relationships/comments',
113
+ 'related' => 'http://example.com/articles/1/comments'
114
+ },
115
+ 'data' => [
116
+ { 'type' => 'comments', 'id' => '5' },
117
+ { 'type' => 'comments', 'id' => '12' }
118
+ ]
119
+ }
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
125
+ ## Contributing
126
+
127
+ 1. Fork the [official repository](https://github.com/beauby/jsonapi_parser/tree/master).
128
+ 2. Make your changes in a topic branch.
129
+ 3. Send a pull request.
130
+
131
+ Notes:
132
+
133
+ * Contributions without tests won't be accepted.
134
+ * Please don't update the Gem version.
135
+
136
+ ## License
137
+
138
+ jsonapi_parser is Copyright © 2016 Lucas Hosseini.
139
+
140
+ It is free software, and may be redistributed under the terms specified in the
141
+ [LICENSE](LICENSE) file.
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task default: :spec
7
+ task test: :spec
@@ -0,0 +1,25 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'jsonapi/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'jsonapi'
7
+ spec.version = JSONAPI::VERSION
8
+ spec.authors = ['Lucas Hosseini']
9
+ spec.email = ['lucas.hosseini@gmail.com']
10
+ spec.summary = 'Parse and validate JSON API documents'
11
+ spec.description = 'Tools for handling JSON API documents'
12
+ spec.homepage = 'https://github.com/beauby/jsonapi'
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(spec)/})
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_dependency 'json', '~>1.8'
21
+ spec.add_dependency 'activesupport', '>=3.0'
22
+
23
+ spec.add_development_dependency 'rake', '>=0.9'
24
+ spec.add_development_dependency 'rspec', '~>3.4'
25
+ end
@@ -0,0 +1,41 @@
1
+ module JSONAPI
2
+ # c.f. http://jsonapi.org/format/#document-resource-object-attributes
3
+ class Attributes
4
+ include Enumerable
5
+
6
+ def initialize(attributes_hash, options = {})
7
+ fail InvalidDocument,
8
+ "the value of 'attributes' MUST be an object" unless
9
+ attributes_hash.is_a?(Hash)
10
+
11
+ @hash = attributes_hash
12
+ @attributes = {}
13
+ attributes_hash.each do |attr_name, attr_val|
14
+ @attributes[attr_name.to_s] = attr_val
15
+ define_singleton_method(attr_name) do
16
+ @attributes[attr_name.to_s]
17
+ end
18
+ end
19
+ end
20
+
21
+ def to_hash
22
+ @hash
23
+ end
24
+
25
+ def each(&block)
26
+ @attributes.each(&block)
27
+ end
28
+
29
+ def [](attr_name)
30
+ @attributes[attr_name.to_s]
31
+ end
32
+
33
+ def defined?(attr_name)
34
+ @attributes.key?(attr_name.to_s)
35
+ end
36
+
37
+ def keys
38
+ @attributes.keys
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,126 @@
1
+ module JSONAPI
2
+ # c.f. http://jsonapi.org/format/#document-top-level
3
+ class Document
4
+ attr_reader :data, :meta, :errors, :json_api, :links, :included
5
+
6
+ def initialize(document_hash, options = {})
7
+ @hash = document_hash
8
+ @options = options
9
+ @data_defined = document_hash.key?('data')
10
+ @data = parse_data(document_hash['data']) if @data_defined
11
+ @meta_defined = document_hash.key?('meta')
12
+ @meta = parse_meta(document_hash['meta']) if @meta_defined
13
+ @errors_defined = document_hash.key?('errors')
14
+ @errors = parse_errors(document_hash['errors']) if @errors_defined
15
+ @jsonapi_defined = document_hash.key?('jsonapi')
16
+ @jsonapi = JsonApi.new(document_hash['jsonapi'], @options) if
17
+ @jsonapi_defined
18
+ @links_hash = document_hash['links'] || {}
19
+ @links = Links.new(@links_hash, @options)
20
+ @included_defined = document_hash.key?('included')
21
+ @included = parse_included(document_hash['included']) if
22
+ @included_defined
23
+
24
+ validate!
25
+ end
26
+
27
+ def to_hash
28
+ @hash
29
+ end
30
+
31
+ def collection?
32
+ @data.is_a?(Array)
33
+ end
34
+
35
+ private
36
+
37
+ def validate!
38
+ case
39
+ when !@data_defined && !@meta_defined && !@errors_defined
40
+ fail InvalidDocument,
41
+ "a document MUST contain at least one of 'data', 'meta', or" \
42
+ " or 'errors' at top-level"
43
+ when @data_defined && @errors_defined
44
+ fail InvalidDocument,
45
+ "'data' and 'errors' MUST NOT coexist in the same document"
46
+ when !@data_defined && @included_defined
47
+ fail InvalidDocument, "'included' MUST NOT be present unless 'data' is"
48
+ when @options[:verify_duplicates] && duplicates?
49
+ fail InvalidDocument,
50
+ "resources MUST NOT appear both in 'data' and 'included'"
51
+ when @options[:verify_linkage] && !full_linkage?
52
+ fail InvalidDocument,
53
+ "resources in 'included' MUST respect full-linkage"
54
+ end
55
+ end
56
+
57
+ def duplicates?
58
+ resources = Set.new
59
+
60
+ (Array(data) + Array(included)).each do |resource|
61
+ return true unless resources.add?([resource.type, resource.id])
62
+ end
63
+
64
+ false
65
+ end
66
+
67
+ def full_linkage?
68
+ return true unless @included
69
+
70
+ reachable = Set.new
71
+ # NOTE(lucas): Does Array() already dup?
72
+ queue = Array(data).dup
73
+ included_resources = Hash[included.map { |r| [[r.type, r.id], r] }]
74
+ queue.each { |resource| reachable << [resource.type, resource.id] }
75
+
76
+ traverse = lambda do |rel|
77
+ ri = [rel.type, rel.id]
78
+ return unless included_resources[ri]
79
+ return unless reachable.add?(ri)
80
+ queue << included_resources[ri]
81
+ end
82
+
83
+ until queue.empty?
84
+ resource = queue.pop
85
+ resource.relationships.each do |_, rel|
86
+ Array(rel.data).map(&traverse)
87
+ end
88
+ end
89
+
90
+ included_resources.keys.all? { |ri| reachable.include?(ri) }
91
+ end
92
+
93
+ def parse_data(data_hash)
94
+ collection = data_hash.is_a?(Array)
95
+ if collection
96
+ data_hash.map { |h| Resource.new(h, @options.merge(id_optional: true)) }
97
+ elsif data_hash.nil?
98
+ nil
99
+ else
100
+ Resource.new(data_hash, @options.merge(id_optional: true))
101
+ end
102
+ end
103
+
104
+ def parse_meta(meta_hash)
105
+ fail InvalidDocument, "the value of 'meta' MUST be an object" unless
106
+ meta_hash.is_a?(Hash)
107
+ meta_hash
108
+ end
109
+
110
+ def parse_included(included_hash)
111
+ fail InvalidDocument,
112
+ "the value of 'included' MUST be an array of resource objects" unless
113
+ included_hash.is_a?(Array)
114
+
115
+ included_hash.map { |h| Resource.new(h, @options) }
116
+ end
117
+
118
+ def parse_errors(errors_hash)
119
+ fail InvalidDocument,
120
+ "the value of 'errors' MUST be an array of error objects" unless
121
+ errors_hash.is_a?(Array)
122
+
123
+ errors_hash.map { |h| Error.new(h, @options) }
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,27 @@
1
+ module JSONAPI
2
+ # c.f. http://jsonapi.org/format/#error-objects
3
+ class Error
4
+ attr_reader :id, :links, :status, :code, :title, :detail, :source, :meta
5
+
6
+ def initialize(error_hash, options = {})
7
+ fail InvalidDocument,
8
+ "the value of 'errors' MUST be an array of error objects" unless
9
+ error_hash.is_a?(Hash)
10
+
11
+ @hash = error_hash
12
+ @id = error_hash['id'] if error_hash.key?('id')
13
+ links_hash = error_hash['links'] || {}
14
+ @links = Links.new(links_hash, options)
15
+ @status = error_hash['status'] if error_hash.key?('status')
16
+ @code = error_hash['code'] if error_hash.key?('code')
17
+ @title = error_hash['title'] if error_hash.key?('title')
18
+ @detail = error_hash['detail'] if error_hash.key?('detail')
19
+ @source = error_hash['source'] if error_hash.key?('source')
20
+ @meta = error_hash['meta'] if error_hash.key?('meta')
21
+ end
22
+
23
+ def to_hash
24
+ @hash
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,4 @@
1
+ module JSONAPI
2
+ class InvalidDocument < StandardError
3
+ end
4
+ end
@@ -0,0 +1,57 @@
1
+ require 'active_support/core_ext/hash/deep_merge'
2
+
3
+ module JSONAPI
4
+ class IncludeDirective
5
+ # Utilities to create an IncludeDirective hash from various types of
6
+ # inputs.
7
+ module Parser
8
+ module_function
9
+
10
+ # @api private
11
+ def parse_include_args(include_args)
12
+ case include_args
13
+ when Symbol
14
+ { include_args => {} }
15
+ when Hash
16
+ parse_hash(include_args)
17
+ when Array
18
+ parse_array(include_args)
19
+ when String
20
+ parse_string(include_args)
21
+ else
22
+ {}
23
+ end
24
+ end
25
+
26
+ # @api private
27
+ def parse_string(include_string)
28
+ include_string.split(',')
29
+ .map(&:strip)
30
+ .each_with_object({}) do |path, hash|
31
+ hash.deep_merge!(parse_path_string(path))
32
+ end
33
+ end
34
+
35
+ # @api private
36
+ def parse_path_string(include_path)
37
+ include_path.split('.')
38
+ .reverse
39
+ .reduce({}) { |a, e| { e.to_sym => a } }
40
+ end
41
+
42
+ # @api private
43
+ def parse_hash(include_hash)
44
+ include_hash.each_with_object({}) do |(key, value), hash|
45
+ hash[key.to_sym] = parse_include_args(value)
46
+ end
47
+ end
48
+
49
+ # @api private
50
+ def parse_array(include_array)
51
+ include_array.each_with_object({}) do |x, hash|
52
+ hash.deep_merge!(parse_include_args(x))
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,67 @@
1
+ require 'jsonapi/include_directive/parser'
2
+
3
+ module JSONAPI
4
+ # Represent a recursive set of include directives
5
+ # (c.f. http://jsonapi.org/format/#fetching-includes)
6
+ #
7
+ # Addition to the spec: two wildcards, namely '*' and '**'.
8
+ # The former stands for any one level of relationship, and the latter stands
9
+ # for any number of levels of relationships.
10
+ # @example 'posts.*' # => Include related posts, and all the included posts'
11
+ # related resources.
12
+ # @example 'posts.**' # => Include related posts, and all the included
13
+ # posts' related resources, and their related resources, recursively.
14
+ class IncludeDirective
15
+ # @param include_args (see Parser.include_hash_from_include_args)
16
+ def initialize(include_args, options = {})
17
+ include_hash = Parser.parse_include_args(include_args)
18
+ @hash = include_hash.each_with_object({}) do |(key, value), hash|
19
+ hash[key] = self.class.new(value, options)
20
+ end
21
+ @options = options
22
+ end
23
+
24
+ # @param key [Symbol, String]
25
+ def key?(key)
26
+ @hash.key?(key.to_sym) ||
27
+ (@options[:allow_wildcard] && (@hash.key?(:*) || @hash.key?(:**)))
28
+ end
29
+
30
+ # @param key [Symbol, String]
31
+ # @return [IncludeDirective, nil]
32
+ def [](key)
33
+ case
34
+ when @hash.key?(key.to_sym)
35
+ @hash[key.to_sym]
36
+ when @options[:allow_wildcard] && @hash.key?(:**)
37
+ self.class.new({ :** => {} }, @options)
38
+ when @options[:allow_wildcard] && @hash.key?(:*)
39
+ @hash[:*]
40
+ end
41
+ end
42
+
43
+ # @return [Hash{Symbol => Hash}]
44
+ def to_hash
45
+ @hash.each_with_object({}) do |(key, value), hash|
46
+ hash[key] = value.to_hash
47
+ end
48
+ end
49
+
50
+ # @return [String]
51
+ def to_string
52
+ string_array = @hash.map do |(key, value)|
53
+ string_value = value.to_string
54
+ if string_value == ''
55
+ key.to_s
56
+ else
57
+ string_value
58
+ .split(',')
59
+ .map { |x| key.to_s + '.' + x }
60
+ .join(',')
61
+ end
62
+ end
63
+
64
+ string_array.join(',')
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,19 @@
1
+ module JSONAPI
2
+ # c.f. http://jsonapi.org/format/#document-jsonapi-object
3
+ class JsonApi
4
+ attr_reader :version, :meta
5
+
6
+ def initialize(jsonapi_hash, options = {})
7
+ fail InvalidDocument, "the value of 'jsonapi' MUST be an object" unless
8
+ jsonapi_hash.is_a?(Hash)
9
+
10
+ @hash = jsonapi_hash
11
+ @version = jsonapi_hash['version'] if jsonapi_hash.key?('meta')
12
+ @meta = jsonapi_hash['meta'] if jsonapi_hash.key?('meta')
13
+ end
14
+
15
+ def to_hash
16
+ @hash
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ module JSONAPI
2
+ # c.f. http://jsonapi.org/format/#document-links
3
+ class Link
4
+ attr_reader :value, :href, :meta
5
+
6
+ def initialize(link_hash, options = {})
7
+ @hash = link_hash
8
+
9
+ validate!(link_hash)
10
+ @value = link_hash
11
+ return unless link_hash.is_a?(Hash)
12
+
13
+ @href = link_hash['href']
14
+ @meta = link_hash['meta']
15
+ end
16
+
17
+ def to_hash
18
+ @hash
19
+ end
20
+
21
+ private
22
+
23
+ def validate!(link_hash)
24
+ case
25
+ when !link_hash.is_a?(String) && !link_hash.is_a?(Hash)
26
+ fail InvalidDocument,
27
+ "a 'link' object MUST be either a string or an object"
28
+ when link_hash.is_a?(Hash) && (!link_hash.key?('href') ||
29
+ !link_hash['href'].is_a?(String))
30
+ fail InvalidDocument,
31
+ "a 'link' object MUST be either a string or an object containing" \
32
+ " an 'href' string"
33
+ when link_hash.is_a?(Hash) && (!link_hash.key?('meta') ||
34
+ !link_hash['meta'].is_a?(Hash))
35
+ fail InvalidDocument,
36
+ "a 'link' object MUST be either a string or an object containing" \
37
+ " an 'meta' object"
38
+ end
39
+ end
40
+ end
41
+ end