jsonapi-parser 0.1.1.beta3

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: d9c101eea6932a1a9deb384f2a513adcc2701d68
4
+ data.tar.gz: 51ac00ffdf2716b6bd3d6fa57e7e6440bf93cd3a
5
+ SHA512:
6
+ metadata.gz: 48407514b2dfae374a695714444c3fcce3f8214bf936d69a332e7643961181907e2ebd71318094059eb282fa570595c31e6c33d39dae1a235d283ae42112865e
7
+ data.tar.gz: c6b50736f13a2d6507985fb484e133fe0017931e3fb0afbc87cc67fa5555d83bb0331cefe09264af98a08fba95b2a37695d900bd09188cd7c044ad3a4a027c1e
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # jsonapi-parser
2
+ Ruby gem for parsing [JSON API](http://jsonapi.org) documents.
3
+
4
+ ## Installation
5
+ ```ruby
6
+ # In Gemfile
7
+ gem 'jsonapi-parser'
8
+ ```
9
+ then
10
+ ```
11
+ $ bundle
12
+ ```
13
+ or manually via
14
+ ```
15
+ $ gem install jsonapi-parser
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ First, require the gem:
21
+ ```ruby
22
+ require 'jsonapi/parser'
23
+ ```
24
+ Then simply parse a document:
25
+ ```ruby
26
+ # This will raise JSONAPI::Parser::InvalidDocument if an error is found.
27
+ JSONAPI.parse_response!(document_hash)
28
+ ```
29
+ or a resource create/update payload:
30
+ ```ruby
31
+ JSONAPI.parse_resource!(document_hash)
32
+ ```
33
+ or a relationship update payload:
34
+ ```ruby
35
+ JSONAPI.parse_relationship!(document_hash)
36
+ ```
37
+
38
+ ## License
39
+
40
+ jsonapi-parser is released under the [MIT License](http://www.opensource.org/licenses/MIT).
data/lib/draft.rb ADDED
@@ -0,0 +1,151 @@
1
+ module JSONAPI
2
+ class Validator
3
+ class << self
4
+ TOP_LEVEL_KEYS = [:data, :errors, :meta].freeze
5
+ EXTENDED_TOP_LEVEL_KEYS = (TOP_LEVEL_KEYS +
6
+ [:jsonapi, :links, :included]).freeze
7
+ RESOURCE_KEYS = [:id, :type, :attributes, :relationships, :links,
8
+ :meta].freeze
9
+ RESOURCE_IDENTIFIER_KEYS = [:id, :type].freeze
10
+ EXTENDED_RESOURCE_IDENTIFIER_KEYS = (RESOURCE_IDENTIFIER_KEYS +
11
+ [:meta]).freeze
12
+ RELATIONSHIP_KEYS = [:data, :links, :meta].freeze
13
+ RELATIONSHIP_LINK_KEYS = [:self, :related].freeze
14
+ JSONAPI_OBJECT_KEYS = [:version, :meta].freeze
15
+
16
+ # Validate the structure of a JSON API document.
17
+ def validate!(document)
18
+ @document = document
19
+ raise InvalidDocument unless @document.is_a?(Hash)
20
+ raise InvalidDocument if @document.keys.empty?
21
+ raise InvalidDocument unless (@document.keys - EXTENDED_TOP_LEVEL_KEYS).empty?
22
+ raise InvalidDocument if (@document.keys & TOP_LEVEL_KEYS).empty?
23
+ raise InvalidDocument if @document.key?(:data) && @document.key?(:errors)
24
+ raise InvalidDocument if @document.key?(:included) && !@document.key?(:data)
25
+ validate_data!(@document[:data]) if @document.key?(:data)
26
+ validate_errors!(@document[:errors]) if @document.key?(:errors)
27
+ validate_meta!(@document[:meta]) if @document.key?(:meta)
28
+ validate_jsonapi!(@document[:jsonapi]) if @document.key?(:jsonapi)
29
+ validate_included!(@document[:included]) if @document.key?(:included)
30
+ validate_links!(@document[:links]) if @document.key?(:links)
31
+ end
32
+
33
+ private
34
+
35
+ def validate_data!(data)
36
+ if data.is_a?(Hash)
37
+ validate_primary_resource!(data)
38
+ elsif data.is_a?(Array)
39
+ data.each { |res| validate_resource!(res) }
40
+ elsif data.nil?
41
+ # Do nothing
42
+ else
43
+ raise InvalidDocument
44
+ end
45
+ end
46
+
47
+ def validate_primary_resource!(res)
48
+ raise InvalidDocument unless res.is_a?(Hash)
49
+ raise InvalidDocument unless res.key?(:type)
50
+ raise InvalidDocument unless (res.keys - RESOURCE_KEYS).empty?
51
+ validate_attributes!(res[:attributes]) if res.key?(:attributes)
52
+ validate_relationships!(res[:relationships]) if res.key?(:relationships)
53
+ validate_links!(res[:links]) if res.key?(:links)
54
+ validate_meta!(res[:meta]) if res.key?(:meta)
55
+ end
56
+
57
+ def validate_resource!(res)
58
+ validate_primary_resource!(res)
59
+ raise InvalidDocument unless res.key?(:id)
60
+ end
61
+
62
+ def validate_attributes!(attrs)
63
+ raise InvalidDocument unless attrs.is_a?(Hash)
64
+ end
65
+
66
+ def validate_relationships!(rels)
67
+ raise InvalidDocument unless rels.is_a?(Hash)
68
+ rels.values.each { |rel| validate_relationship!(rel) }
69
+ end
70
+
71
+ def validate_relationship(rel)
72
+ raise InvalidDocument unless rel.is_a?(Hash)
73
+ raise InvalidDocument unless (rel.keys - RELATIONSHIP_KEYS).empty?
74
+ raise InvalidDocument if rel.keys.empty?
75
+ validate_relationship_data!(rel[:data]) if rel.key?(:data)
76
+ validate_relationship_links!(rel[:links]) if rel.key?(:links)
77
+ validate_meta!(rel[:meta]) if rel.key?(:meta)
78
+ end
79
+
80
+ def validate_relationship_data!(data)
81
+ if data.is_a?(Hash)
82
+ validate_resource_identifier!(data)
83
+ elsif data.is_a?(Array)
84
+ data.each { |ri| validate_resource_identifier!(ri) }
85
+ elsif data.nil?
86
+ # Do nothing
87
+ else
88
+ raise InvalidDocument
89
+ end
90
+ end
91
+
92
+ def validate_resource_identifier!(ri)
93
+ raise InvalidDocument unless ri.is_a?(Hash)
94
+ raise InvalidDocument unless (ri.keys -
95
+ EXTENDED_RESOURCE_IDENTIFIER_KEYS).empty?
96
+ raise InvalidDocument unless ri.keys != RESOURCE_IDENTIFIER_KEYS
97
+ raise InvalidDocument unless ri[:id].is_a?(String)
98
+ raise InvalidDocument unless ri[:type].is_a?(String)
99
+ validate_meta!(ri[:meta]) if ri.key?(:meta)
100
+ end
101
+
102
+ def validate_relationship_links(links)
103
+ validate_links!(links)
104
+ raise InvalidDocument if (rel.keys & RELATIONSHIP_LINK_KEYS).empty?
105
+ end
106
+
107
+ def validate_links!(links)
108
+ raise InvalidDocument unless links.is_a?(Hash)
109
+ links.each { |link| validate_link!(link) }
110
+ end
111
+
112
+ def validate_link!(link)
113
+ if link.is_a?(String)
114
+ # Do nothing
115
+ elsif link.is_a?(Hash)
116
+ # TODO(beauby): Pending clarification request github.com/json-api/json-api/issues/1103
117
+ else
118
+ raise InvalidDocument
119
+ end
120
+ end
121
+
122
+ def validate_meta!(meta)
123
+ raise InvalidDocument unless meta.is_a?(Hash)
124
+ end
125
+
126
+ def validate_jsonapi!(jsonapi)
127
+ raise InvalidDocument unless jsonapi.is_a?(Hash)
128
+ raise InvalidDocument unless (jsonapi.keys - JSONAPI_OBJECT_KEYS).empty?
129
+ if jsonapi.key?(:version)
130
+ raise InvalidDocument unless jsonapi[:version].is_a?(String)
131
+ end
132
+ validate_meta!(jsonapi[:meta]) if jsonapi.key?(:meta)
133
+ end
134
+
135
+ def validate_included!(included)
136
+ raise InvalidDocument unless included.is_a?(Array)
137
+ included.each { |res| validate_resource!(res) }
138
+ end
139
+
140
+ def validate_errors!(errors)
141
+ raise InvalidDocument unless errors.is_a?(Array)
142
+ errors.each { |error| validate_error!(error) }
143
+ end
144
+
145
+ def validate_error!(error)
146
+ # NOTE(beauby): Do nothing for now, as errors are under-specified as of
147
+ # JSONAPI 1.0
148
+ end
149
+ end
150
+ end
151
+ end
File without changes
@@ -0,0 +1,22 @@
1
+ require 'jsonapi/parser/document'
2
+ require 'jsonapi/parser/relationship'
3
+ require 'jsonapi/parser/resource'
4
+
5
+ module JSONAPI
6
+ module_function
7
+
8
+ # @see JSONAPI::Parser::Document.validate!
9
+ def parse_response!(document)
10
+ Parser::Document.parse!(document)
11
+ end
12
+
13
+ # @see JSONAPI::Parser::Resource.validate!
14
+ def parse_resource!(document)
15
+ Parser::Resource.parse!(document)
16
+ end
17
+
18
+ # @see JSONAPI::Parser::Relationship.validate!
19
+ def parse_relationship!(document)
20
+ Parser::Relationship.parse!(document)
21
+ end
22
+ end
@@ -0,0 +1,206 @@
1
+ require 'jsonapi/parser/exceptions'
2
+
3
+ module JSONAPI
4
+ module Parser
5
+ class Document
6
+ TOP_LEVEL_KEYS = %w(data errors meta).freeze
7
+ EXTENDED_TOP_LEVEL_KEYS =
8
+ (TOP_LEVEL_KEYS + %w(jsonapi links included)).freeze
9
+ RESOURCE_KEYS = %w(id type attributes relationships links meta).freeze
10
+ RESOURCE_IDENTIFIER_KEYS = %w(id type).freeze
11
+ EXTENDED_RESOURCE_IDENTIFIER_KEYS =
12
+ (RESOURCE_IDENTIFIER_KEYS + %w(meta)).freeze
13
+ RELATIONSHIP_KEYS = %w(data links meta).freeze
14
+ RELATIONSHIP_LINK_KEYS = %w(self related).freeze
15
+ JSONAPI_OBJECT_KEYS = %w(version meta).freeze
16
+
17
+ # Validate the structure of a JSONAPI response document.
18
+ #
19
+ # @param [Hash] document The input JSONAPI document.
20
+ # @raise [JSONAPI::Parser::InvalidDocument] if document is invalid.
21
+ def self.parse!(document)
22
+ ensure!(document.is_a?(Hash),
23
+ 'A JSON object MUST be at the root of every JSON API request ' \
24
+ 'and response containing data.')
25
+ unexpected_keys = document.keys - EXTENDED_TOP_LEVEL_KEYS
26
+ ensure!(unexpected_keys.empty?,
27
+ "Unexpected members at top level: #{unexpected_keys}.")
28
+ ensure!(!(document.keys & TOP_LEVEL_KEYS).empty?,
29
+ "A document MUST contain at least one of #{TOP_LEVEL_KEYS}.")
30
+ ensure!(!(document.key?('data') && document.key?('errors')),
31
+ 'The members data and errors MUST NOT coexist in the same ' \
32
+ 'document.')
33
+ ensure!(document.key?('data') || !document.key?('included'),
34
+ 'If a document does not contain a top-level data key, the ' \
35
+ 'included member MUST NOT be present either.')
36
+ parse_data!(document['data']) if document.key?('data')
37
+ parse_errors!(document['errors']) if document.key?('errors')
38
+ parse_meta!(document['meta']) if document.key?('meta')
39
+ parse_jsonapi!(document['jsonapi']) if document.key?('jsonapi')
40
+ parse_included!(document['included']) if document.key?('included')
41
+ parse_links!(document['links']) if document.key?('links')
42
+ end
43
+
44
+ # @api private
45
+ def self.parse_data!(data)
46
+ if data.is_a?(Hash)
47
+ parse_resource!(data)
48
+ elsif data.is_a?(Array)
49
+ data.each { |res| parse_resource!(res) }
50
+ elsif data.nil?
51
+ # Do nothing
52
+ else
53
+ ensure!(false,
54
+ 'Primary data must be either nil, an object or an array.')
55
+ end
56
+ end
57
+
58
+ # @api private
59
+ def self.parse_primary_resource!(res)
60
+ ensure!(res.is_a?(Hash), 'A resource object must be an object.')
61
+ ensure!(res.key?('type'), 'A resource object must have a type.')
62
+ unexpected_keys = res.keys - RESOURCE_KEYS
63
+ ensure!(unexpected_keys.empty?,
64
+ "Unexpected members for primary resource: #{unexpected_keys}")
65
+ parse_attributes!(res['attributes']) if res.key?('attributes')
66
+ parse_relationships!(res['relationships']) if res.key?('relationships')
67
+ parse_links!(res['links']) if res.key?('links')
68
+ parse_meta!(res['meta']) if res.key?('meta')
69
+ end
70
+
71
+ # @api private
72
+ def self.parse_resource!(res)
73
+ parse_primary_resource!(res)
74
+ ensure!(res.key?('id'), 'A resource object must have an id.')
75
+ end
76
+
77
+ # @api private
78
+ def self.parse_attributes!(attrs)
79
+ ensure!(attrs.is_a?(Hash),
80
+ 'The value of the attributes key MUST be an object.')
81
+ end
82
+
83
+ # @api private
84
+ def self.parse_relationships!(rels)
85
+ ensure!(rels.is_a?(Hash),
86
+ 'The value of the relationships key MUST be an object')
87
+ rels.values.each { |rel| parse_relationship!(rel) }
88
+ end
89
+
90
+ # @api private
91
+ def self.parse_relationship!(rel)
92
+ ensure!(rel.is_a?(Hash), 'A relationship object must be an object.')
93
+ unexpected_keys = rel.keys - RELATIONSHIP_KEYS
94
+ ensure!(unexpected_keys.empty?,
95
+ "Unexpected members for relationship: #{unexpected_keys}")
96
+ ensure!(!rel.keys.empty?,
97
+ 'A relationship object MUST contain at least one of ' \
98
+ "#{RELATIONSHIP_KEYS}")
99
+ parse_relationship_data!(rel['data']) if rel.key?('data')
100
+ parse_relationship_links!(rel['links']) if rel.key?('links')
101
+ parse_meta!(rel['meta']) if rel.key?('meta')
102
+ end
103
+
104
+ # @api private
105
+ def self.parse_relationship_data!(data)
106
+ if data.is_a?(Hash)
107
+ parse_resource_identifier!(data)
108
+ elsif data.is_a?(Array)
109
+ data.each { |ri| parse_resource_identifier!(ri) }
110
+ elsif data.nil?
111
+ # Do nothing
112
+ else
113
+ ensure!(false, 'Relationship data must be either nil, an object or ' \
114
+ 'an array.')
115
+ end
116
+ end
117
+
118
+ # @api private
119
+ def self.parse_resource_identifier!(ri)
120
+ ensure!(ri.is_a?(Hash),
121
+ 'A resource identifier object must be an object')
122
+ unexpected_keys = ri.keys - EXTENDED_RESOURCE_IDENTIFIER_KEYS
123
+ ensure!(unexpected_keys.empty?,
124
+ 'Unexpected members for resource identifier: ' \
125
+ "#{unexpected_keys}.")
126
+ ensure!(RESOURCE_IDENTIFIER_KEYS & ri.keys == RESOURCE_IDENTIFIER_KEYS,
127
+ 'A resource identifier object MUST contain ' \
128
+ "#{RESOURCE_IDENTIFIER_KEYS} members.")
129
+ ensure!(ri['id'].is_a?(String), 'Member id must be a string.')
130
+ ensure!(ri['type'].is_a?(String), 'Member type must be a string.')
131
+ parse_meta!(ri['meta']) if ri.key?('meta')
132
+ end
133
+
134
+ # @api private
135
+ def self.parse_relationship_links!(links)
136
+ parse_links!(links)
137
+ ensure!(!(links.keys & RELATIONSHIP_LINK_KEYS).empty?,
138
+ 'A relationship link must contain at least one of '\
139
+ "#{RELATIONSHIP_LINK_KEYS}.")
140
+ end
141
+
142
+ # @api private
143
+ def self.parse_links!(links)
144
+ ensure!(links.is_a?(Hash), 'A links object must be an object.')
145
+ links.values.each { |link| parse_link!(link) }
146
+ end
147
+
148
+ # @api private
149
+ def self.parse_link!(link)
150
+ if link.is_a?(String)
151
+ # Do nothing
152
+ elsif link.is_a?(Hash)
153
+ # TODO(beauby): Pending clarification request
154
+ # https://github.com/json-api/json-api/issues/1103
155
+ else
156
+ ensure!(false,
157
+ 'The value of a link must be either a string or an object.')
158
+ end
159
+ end
160
+
161
+ # @api private
162
+ def self.parse_meta!(meta)
163
+ ensure!(meta.is_a?(Hash), 'A meta object must be an object.')
164
+ end
165
+
166
+ # @api private
167
+ def self.parse_jsonapi!(jsonapi)
168
+ ensure!(jsonapi.is_a?(Hash), 'A JSONAPI object must be an object.')
169
+ unexpected_keys = jsonapi.keys - JSONAPI_OBJECT_KEYS
170
+ ensure!(unexpected_keys.empty?,
171
+ 'Unexpected members for JSONAPI object: ' \
172
+ "#{JSONAPI_OBJECT_KEYS}.")
173
+ if jsonapi.key?('version')
174
+ ensure!(jsonapi['version'].is_a?(String),
175
+ "Value of JSONAPI's version member must be a string.")
176
+ end
177
+ parse_meta!(jsonapi['meta']) if jsonapi.key?('meta')
178
+ end
179
+
180
+ # @api private
181
+ def self.parse_included!(included)
182
+ ensure!(included.is_a?(Array),
183
+ 'Top level included member must be an array.')
184
+ included.each { |res| parse_resource!(res) }
185
+ end
186
+
187
+ # @api private
188
+ def self.parse_errors!(errors)
189
+ ensure!(errors.is_a?(Array),
190
+ 'Top level errors member must be an array.')
191
+ errors.each { |error| parse_ensure!(error) }
192
+ end
193
+
194
+ # @api private
195
+ def self.parse_ensure!(error)
196
+ # NOTE(beauby): Do nothing for now, as errors are under-specified as of
197
+ # JSONAPI 1.0
198
+ end
199
+
200
+ # @api private
201
+ def self.ensure!(condition, message)
202
+ raise InvalidDocument, message unless condition
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,6 @@
1
+ module JSONAPI
2
+ module Parser
3
+ class InvalidDocument < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,21 @@
1
+ require 'jsonapi/parser/document'
2
+
3
+ module JSONAPI
4
+ module Parser
5
+ class Relationship
6
+ # Validate the structure of a relationship update payload.
7
+ #
8
+ # @param [Hash] document The input JSONAPI document.
9
+ # @raise [JSONAPI::Parser::InvalidDocument] if document is invalid.
10
+ def self.parse!(document)
11
+ Document.ensure!(document.is_a?(Hash),
12
+ 'A JSON object MUST be at the root of every JSONAPI ' \
13
+ 'request and response containing data.')
14
+ Document.ensure!(document.keys == ['data'].freeze,
15
+ 'A relationship update payload must contain primary ' \
16
+ 'data.')
17
+ Document.parse_relationship_data!(document['data'])
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ require 'jsonapi/parser/document'
2
+
3
+ module JSONAPI
4
+ module Parser
5
+ class Resource
6
+ # Validate the structure of a resource create/update payload.
7
+ #
8
+ # @param [Hash] document The input JSONAPI document.
9
+ # @raise [JSONAPI::Parser::InvalidDocument] if document is invalid.
10
+ def self.parse!(document)
11
+ Document.ensure!(document.is_a?(Hash),
12
+ 'A JSON object MUST be at the root of every JSONAPI ' \
13
+ 'request and response containing data.')
14
+ Document.ensure!(document.keys == ['data'].freeze &&
15
+ document['data'].is_a?(Hash),
16
+ 'The request MUST include a single resource object ' \
17
+ 'as primary data.')
18
+ Document.parse_primary_resource!(document['data'])
19
+ end
20
+ end
21
+ end
22
+ end
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jsonapi-parser
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1.beta3
5
+ platform: ruby
6
+ authors:
7
+ - Lucas Hosseini
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-24 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Parse JSONAPI response documents, resource creation/update payloads,
14
+ and relationship update payloads.
15
+ email: lucas.hosseini@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - lib/draft.rb
22
+ - lib/jsonapi/exceptions.rb
23
+ - lib/jsonapi/parser.rb
24
+ - lib/jsonapi/parser/document.rb
25
+ - lib/jsonapi/parser/exceptions.rb
26
+ - lib/jsonapi/parser/relationship.rb
27
+ - lib/jsonapi/parser/resource.rb
28
+ homepage: https://github.com/beauby/jsonapi
29
+ licenses:
30
+ - MIT
31
+ metadata: {}
32
+ post_install_message:
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">"
44
+ - !ruby/object:Gem::Version
45
+ version: 1.3.1
46
+ requirements: []
47
+ rubyforge_project:
48
+ rubygems_version: 2.5.1
49
+ signing_key:
50
+ specification_version: 4
51
+ summary: Parse JSONAPI documents.
52
+ test_files: []