jsonapi-parser 0.1.1.beta3

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: 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: []