jsonapi-renderer 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 +7 -0
- data/README.md +91 -0
- data/lib/jsonapi/include_directive/parser.rb +65 -0
- data/lib/jsonapi/include_directive.rb +72 -0
- data/lib/jsonapi/renderer.rb +118 -0
- metadata +47 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3f02425bfc2d75e4ea21cd4ecb42b6378b4c6939
|
4
|
+
data.tar.gz: 14400fddf957704ea29b97b422a0f814bad5f3a2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 57a9d3c9d252fa70c29ee3734b322c8254b527b13661c226184cd3d4304eaaf9ae12b88a9c105a21f826b61f07fc71f6cd02e31f19c3a634001a4f178e204a26
|
7
|
+
data.tar.gz: c620d458eb042cc288c5b461af20181acc0e75dd9d0c0509c9f3bc6b9cb7edb1711db86bdbc1aa5c5805f882c148a1c4d57e708bcccfcb6fa4962a2b628c0555
|
data/README.md
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# jsonapi-renderer
|
2
|
+
Ruby gem for rendering [JSON API](http://jsonapi.org) documents.
|
3
|
+
|
4
|
+
## Installation
|
5
|
+
```ruby
|
6
|
+
# In Gemfile
|
7
|
+
gem 'jsonapi-renderer'
|
8
|
+
```
|
9
|
+
then
|
10
|
+
```
|
11
|
+
$ bundle
|
12
|
+
```
|
13
|
+
or manually via
|
14
|
+
```
|
15
|
+
$ gem install jsonapi-renderer
|
16
|
+
```
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
First, require the gem:
|
21
|
+
```ruby
|
22
|
+
require 'jsonapi/renderer'
|
23
|
+
```
|
24
|
+
|
25
|
+
### Rendering resources
|
26
|
+
|
27
|
+
A resource here is any class that implements the following interface:
|
28
|
+
```ruby
|
29
|
+
class ResourceInterface
|
30
|
+
# Returns the type of the resource.
|
31
|
+
# @return [String]
|
32
|
+
def jsonapi_type; end
|
33
|
+
|
34
|
+
# Returns the id of the resource.
|
35
|
+
# @return [String]
|
36
|
+
def jsonapi_id; end
|
37
|
+
|
38
|
+
# Returns a hash containing, for each included relationship, the resource(s)
|
39
|
+
# to be included from that one.
|
40
|
+
# @param [Array<Symbol>] included_relationships The keys of the relationships
|
41
|
+
# to be included.
|
42
|
+
# @return [Hash{Symbol => #ResourceInterface, Array<#ResourceInterface>}]
|
43
|
+
def jsonapi_related(included_relationships); end
|
44
|
+
|
45
|
+
# Returns a JSON API-compliant representation of the resource as a hash.
|
46
|
+
# @param [Hash] options
|
47
|
+
# @option [Array<Symbol>, Nil] fields The requested fields, or nil.
|
48
|
+
# @option [Array<Symbol>, Nil] included The requested included
|
49
|
+
# relationships, or nil.
|
50
|
+
# @return [Hash]
|
51
|
+
def as_jsonapi(options = {}); end
|
52
|
+
```
|
53
|
+
|
54
|
+
#### Rendering a single resource
|
55
|
+
```ruby
|
56
|
+
JSONAPI.render(resource,
|
57
|
+
include: include_string,
|
58
|
+
fields: fields_hash,
|
59
|
+
meta: meta_hash,
|
60
|
+
links: links_hash)
|
61
|
+
```
|
62
|
+
|
63
|
+
This returns a JSON API compliant hash representing the described document.
|
64
|
+
|
65
|
+
#### Rendering a collection of resources
|
66
|
+
```ruby
|
67
|
+
JSONAPI.render(resources,
|
68
|
+
include: include_string,
|
69
|
+
fields: fields_hash,
|
70
|
+
meta: meta_hash,
|
71
|
+
links: links_hash)
|
72
|
+
```
|
73
|
+
|
74
|
+
This returns a JSON API compliant hash representing the described document.
|
75
|
+
|
76
|
+
### Rendering errors
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
JSONAPI.render_errors(errors,
|
80
|
+
meta: meta_hash,
|
81
|
+
links: links_hash)
|
82
|
+
```
|
83
|
+
|
84
|
+
where `errors` is an array of objects implementing the `as_jsonapi` method, that
|
85
|
+
returns a JSON API-compliant representation of the error.
|
86
|
+
|
87
|
+
This returns a JSON API compliant hash representing the described document.
|
88
|
+
|
89
|
+
## License
|
90
|
+
|
91
|
+
jsonapi-renderer is released under the [MIT License](http://www.opensource.org/licenses/MIT).
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
class IncludeDirective
|
3
|
+
# Utilities to create an IncludeDirective hash from various types of
|
4
|
+
# inputs.
|
5
|
+
module Parser
|
6
|
+
module_function
|
7
|
+
|
8
|
+
# @api private
|
9
|
+
def parse_include_args(include_args)
|
10
|
+
case include_args
|
11
|
+
when Symbol
|
12
|
+
{ include_args => {} }
|
13
|
+
when Hash
|
14
|
+
parse_hash(include_args)
|
15
|
+
when Array
|
16
|
+
parse_array(include_args)
|
17
|
+
when String
|
18
|
+
parse_string(include_args)
|
19
|
+
else
|
20
|
+
{}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @api private
|
25
|
+
def parse_string(include_string)
|
26
|
+
include_string.split(',')
|
27
|
+
.each_with_object({}) do |path, hash|
|
28
|
+
deep_merge!(hash, parse_path_string(path))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @api private
|
33
|
+
def parse_path_string(include_path)
|
34
|
+
include_path.split('.')
|
35
|
+
.reverse
|
36
|
+
.reduce({}) { |a, e| { e.to_sym => a } }
|
37
|
+
end
|
38
|
+
|
39
|
+
# @api private
|
40
|
+
def parse_hash(include_hash)
|
41
|
+
include_hash.each_with_object({}) do |(key, value), hash|
|
42
|
+
hash[key.to_sym] = parse_include_args(value)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @api private
|
47
|
+
def parse_array(include_array)
|
48
|
+
include_array.each_with_object({}) do |x, hash|
|
49
|
+
deep_merge!(hash, parse_include_args(x))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# @api private
|
54
|
+
def deep_merge!(src, ext)
|
55
|
+
ext.each do |k, v|
|
56
|
+
if src[k].is_a?(Hash) && v.is_a?(Hash)
|
57
|
+
deep_merge!(src[k], v)
|
58
|
+
else
|
59
|
+
src[k] = v
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,72 @@
|
|
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.parse_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
|
+
# @return [Array<Symbol>]
|
31
|
+
def keys
|
32
|
+
@hash.keys
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param key [Symbol, String]
|
36
|
+
# @return [IncludeDirective, nil]
|
37
|
+
def [](key)
|
38
|
+
case
|
39
|
+
when @hash.key?(key.to_sym)
|
40
|
+
@hash[key.to_sym]
|
41
|
+
when @options[:allow_wildcard] && @hash.key?(:**)
|
42
|
+
self.class.new({ :** => {} }, @options)
|
43
|
+
when @options[:allow_wildcard] && @hash.key?(:*)
|
44
|
+
@hash[:*]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Hash{Symbol => Hash}]
|
49
|
+
def to_hash
|
50
|
+
@hash.each_with_object({}) do |(key, value), hash|
|
51
|
+
hash[key] = value.to_hash
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [String]
|
56
|
+
def to_string
|
57
|
+
string_array = @hash.map do |(key, value)|
|
58
|
+
string_value = value.to_string
|
59
|
+
if string_value == ''
|
60
|
+
key.to_s
|
61
|
+
else
|
62
|
+
string_value
|
63
|
+
.split(',')
|
64
|
+
.map { |x| key.to_s + '.' + x }
|
65
|
+
.join(',')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
string_array.join(',')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'jsonapi/include_directive'
|
2
|
+
|
3
|
+
module JSONAPI
|
4
|
+
class Renderer
|
5
|
+
def initialize(resources, options = {})
|
6
|
+
@resources = resources
|
7
|
+
@errors = options[:errors] || false
|
8
|
+
@meta = options[:meta] || nil
|
9
|
+
@links = options[:links] || {}
|
10
|
+
@fields = options[:fields] || {}
|
11
|
+
# NOTE(beauby): Room for some nifty defaults on those.
|
12
|
+
@jsonapi = options[:jsonapi_object] || nil
|
13
|
+
@include = JSONAPI::IncludeDirective.new(options[:include] || {})
|
14
|
+
end
|
15
|
+
|
16
|
+
def as_json
|
17
|
+
return @json unless @json.nil?
|
18
|
+
|
19
|
+
process_resources
|
20
|
+
@json = {}
|
21
|
+
|
22
|
+
if @errors
|
23
|
+
@json[:errors] = @resources.map(&:as_jsonapi)
|
24
|
+
else
|
25
|
+
@json[:data] = @resources.respond_to?(:each) ? @primary : @primary[0]
|
26
|
+
@json[:included] = @included if @included.any?
|
27
|
+
end
|
28
|
+
@json[:links] = @links if @links.any?
|
29
|
+
@json[:meta] = @meta unless @meta.nil?
|
30
|
+
@json[:jsonapi] = @jsonapi unless @jsonapi.nil?
|
31
|
+
|
32
|
+
@json
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def process_resources
|
38
|
+
@primary = []
|
39
|
+
@included = []
|
40
|
+
@hashes = {}
|
41
|
+
@processed = Set.new # NOTE(beauby): Set of [type, id, prefix].
|
42
|
+
@queue = []
|
43
|
+
|
44
|
+
Array(@resources).each do |res|
|
45
|
+
process_resource(res, '', @include, true)
|
46
|
+
@processed.add([res.jsonapi_type, res.jsonapi_id, ''])
|
47
|
+
end
|
48
|
+
until @queue.empty?
|
49
|
+
res, prefix, include_dir = @queue.pop
|
50
|
+
process_resource(res, prefix, include_dir, false)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def merge_resources!(a, b)
|
55
|
+
b[:relationships].each do |name, rel|
|
56
|
+
a[:relationships][name][:data] ||= rel[:data] if rel.key?(:data)
|
57
|
+
(a[:relationships][name][:links] ||= {})
|
58
|
+
.merge!(rel[:links]) if rel.key?(:links)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def process_resource(res, prefix, include_dir, is_primary)
|
63
|
+
ri = [res.jsonapi_type, res.jsonapi_id]
|
64
|
+
hash = res.as_jsonapi(fields: @fields[res.jsonapi_type.to_sym],
|
65
|
+
include: include_dir.keys)
|
66
|
+
if @hashes.key?(ri)
|
67
|
+
merge_resources!(@hashes[ri], hash)
|
68
|
+
else
|
69
|
+
(is_primary ? @primary : @included) << (@hashes[ri] = hash)
|
70
|
+
end
|
71
|
+
process_relationships(res, prefix, include_dir)
|
72
|
+
end
|
73
|
+
|
74
|
+
def process_relationships(res, prefix, include_dir)
|
75
|
+
res.jsonapi_related(include_dir.keys).each do |key, data|
|
76
|
+
Array(data).each do |child_res|
|
77
|
+
child_prefix = "#{prefix}.#{key}"
|
78
|
+
next unless @processed.add?([child_res.jsonapi_type,
|
79
|
+
child_res.jsonapi_id,
|
80
|
+
child_prefix])
|
81
|
+
@queue << [child_res, child_prefix, include_dir[key]]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
module_function
|
88
|
+
|
89
|
+
# Render a success JSON API document.
|
90
|
+
#
|
91
|
+
# @param [(#jsonapi_id, #jsonapi_type, #jsonapi_related, #as_jsonapi),
|
92
|
+
# Array<(#jsonapi_id, #jsonapi_type, #jsonapi_related, #as_jsonapi)>,
|
93
|
+
# nil] resources The primary resource(s) to be rendered.
|
94
|
+
# @param [Hash] options All optional.
|
95
|
+
# @option [String, Hash{Symbol => Hash}] include Relationships to be
|
96
|
+
# included.
|
97
|
+
# @option [Hash{Symbol, Array<Symbol>}] fields List of requested fields
|
98
|
+
# for some or all of the resource types.
|
99
|
+
# @option [Hash] meta Non-standard top-level meta information to be
|
100
|
+
# included.
|
101
|
+
# @option [Hash] links Top-level links to be included.
|
102
|
+
# @option [Hash] jsonapi_object JSON API object.
|
103
|
+
def render(resources, options = {})
|
104
|
+
Renderer.new(resources, options).as_json
|
105
|
+
end
|
106
|
+
|
107
|
+
# Render an error JSON API document.
|
108
|
+
#
|
109
|
+
# @param [Array<#jsonapi_id>] errors Errors to be rendered.
|
110
|
+
# @param [Hash] options All optional.
|
111
|
+
# @option [Hash] meta Non-standard top-level meta information to be
|
112
|
+
# included.
|
113
|
+
# @option [Hash] links Top-level links to be included.
|
114
|
+
# @option [Hash] jsonapi_object JSON API object.
|
115
|
+
def render_errors(errors)
|
116
|
+
Renderer.new(errors, errors: true).as_json
|
117
|
+
end
|
118
|
+
end
|
metadata
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jsonapi-renderer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1.beta1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lucas Hosseini
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-10-02 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Low-level renderer for JSONAPI documents.
|
14
|
+
email: lucas.hosseini@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- README.md
|
20
|
+
- lib/jsonapi/include_directive.rb
|
21
|
+
- lib/jsonapi/include_directive/parser.rb
|
22
|
+
- lib/jsonapi/renderer.rb
|
23
|
+
homepage: https://github.com/beauby/jsonapi
|
24
|
+
licenses:
|
25
|
+
- MIT
|
26
|
+
metadata: {}
|
27
|
+
post_install_message:
|
28
|
+
rdoc_options: []
|
29
|
+
require_paths:
|
30
|
+
- lib
|
31
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.3.1
|
41
|
+
requirements: []
|
42
|
+
rubyforge_project:
|
43
|
+
rubygems_version: 2.5.1
|
44
|
+
signing_key:
|
45
|
+
specification_version: 4
|
46
|
+
summary: Render JSONAPI documents.
|
47
|
+
test_files: []
|