jsonapi 0.1.1.beta1

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.
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