Skip to content

Commit 126a395

Browse files
committed
Initial commit.
0 parents  commit 126a395

15 files changed

+495
-0
lines changed

.gitignore

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

.travis.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
language: ruby
2+
sudo: false
3+
before_install:
4+
- bundle update
5+
rvm:
6+
- 2.1
7+
- 2.2
8+
- 2.3.0
9+
- ruby-head
10+
matrix:
11+
allow_failures:
12+
- rvm: ruby-head

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
source 'https://rubygems.org'
2+
3+
gemspec

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# jsonapi-parser
2+
Ruby gem for validating [JSON API](http://jsonapi.org) documents.
3+
4+
## Status
5+
6+
[![Gem Version](https://badge.fury.io/rb/jsonapi-parser.svg)](https://badge.fury.io/rb/jsonapi-parser)
7+
[![Build Status](https://secure.travis-ci.org/jsonapi-rb/parser.svg?branch=master)](http://travis-ci.org/jsonapi-rb/parser?branch=master)
8+
9+
## Installation
10+
```ruby
11+
# In Gemfile
12+
gem 'jsonapi-parser'
13+
```
14+
then
15+
```
16+
$ bundle
17+
```
18+
or manually via
19+
```
20+
$ gem install jsonapi-parser
21+
```
22+
23+
## Usage
24+
25+
First, require the gem:
26+
```ruby
27+
require 'jsonapi/parser'
28+
```
29+
Then simply parse a document:
30+
```ruby
31+
# This will raise JSONAPI::Parser::InvalidDocument if an error is found.
32+
JSONAPI.parse_response!(document_hash)
33+
```
34+
or a resource create/update payload:
35+
```ruby
36+
JSONAPI.parse_resource!(document_hash)
37+
```
38+
or a relationship update payload:
39+
```ruby
40+
JSONAPI.parse_relationship!(document_hash)
41+
```
42+
43+
## License
44+
45+
jsonapi-parser is released under the [MIT License](http://www.opensource.org/licenses/MIT).

Rakefile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
require 'bundler/gem_tasks'
2+
require 'rspec/core/rake_task'
3+
4+
RSpec::Core::RakeTask.new(:spec) do |t|
5+
t.pattern = Dir.glob('spec/**/*_spec.rb')
6+
end
7+
8+
task default: :test
9+
task test: :spec

VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.1.1.beta3

jsonapi-parser.gemspec

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
version = File.read(File.expand_path('../VERSION', __FILE__)).strip
2+
3+
Gem::Specification.new do |spec|
4+
spec.name = 'jsonapi-parser'
5+
spec.version = version
6+
spec.author = 'Lucas Hosseini'
7+
spec.email = 'lucas.hosseini@gmail.com'
8+
spec.summary = 'Validate JSON API documents.'
9+
spec.description = 'Validate JSONAPI response documents, resource ' \
10+
'creation/update payloads, and relationship ' \
11+
'update payloads.'
12+
spec.homepage = 'https://github.com/jsonapi-rb/parser'
13+
spec.license = 'MIT'
14+
15+
spec.files = Dir['README.md', 'lib/**/*']
16+
spec.require_path = 'lib'
17+
end

lib/jsonapi/parser.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/jsonapi/parser/document.rb

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/jsonapi/parser/exceptions.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module JSONAPI
2+
module Parser
3+
class InvalidDocument < StandardError
4+
end
5+
end
6+
end

lib/jsonapi/parser/relationship.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)